mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 20:03:52 +00:00
Merge remote-tracking branch 'origin/master' into v3
This commit is contained in:
+12
-2
@@ -8,7 +8,14 @@ import cors from 'cors';
|
||||
|
||||
// import utils
|
||||
import { join, resolve } from 'path';
|
||||
import { currentDirectory, environment, externalsStartDirectory, isProduction, resolvedPath } from './setup.js';
|
||||
import {
|
||||
currentDirectory,
|
||||
environment,
|
||||
isProduction,
|
||||
resolveExternalsDirectory,
|
||||
resolveStylesDirectory,
|
||||
resolvedPath,
|
||||
} from './setup.js';
|
||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||
|
||||
// Import Routes
|
||||
@@ -35,6 +42,7 @@ import { eventStore, getInitialPayload } from './stores/EventStore.js';
|
||||
import { PlaybackService } from './services/PlaybackService.js';
|
||||
import { RestorePoint, restoreService } from './services/RestoreService.js';
|
||||
import { messageService } from './services/message-service/MessageService.js';
|
||||
import { populateDemo } from './modules/loadDemo.js';
|
||||
|
||||
console.log(`Starting Ontime version ${ONTIME_VERSION}`);
|
||||
|
||||
@@ -64,7 +72,8 @@ app.use('/ontime', ontimeRouter);
|
||||
app.use('/api', apiRouter);
|
||||
|
||||
// serve static - css
|
||||
app.use('/external', express.static(externalsStartDirectory));
|
||||
app.use('/external/styles', express.static(resolveStylesDirectory));
|
||||
app.use('/external/', express.static(resolveExternalsDirectory));
|
||||
app.use('/external', (req, res) => {
|
||||
res.status(404).send(`${req.originalUrl} not found`);
|
||||
});
|
||||
@@ -129,6 +138,7 @@ export const initAssets = async () => {
|
||||
checkStart(OntimeStartOrder.InitAssets);
|
||||
await dbLoadingProcess;
|
||||
populateStyles();
|
||||
populateDemo();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -97,7 +97,6 @@ describe('safeMerge', () => {
|
||||
language: 'pt',
|
||||
});
|
||||
});
|
||||
|
||||
it('merges the osc key', () => {
|
||||
const newData = {
|
||||
osc: {
|
||||
|
||||
@@ -8,5 +8,9 @@ export const config = {
|
||||
directory: 'styles',
|
||||
filename: 'override.css',
|
||||
},
|
||||
demo: {
|
||||
directory: 'demo',
|
||||
filename: ['app.js', 'index.html', 'styles.css'],
|
||||
},
|
||||
restoreFile: 'ontime.restore',
|
||||
};
|
||||
|
||||
@@ -137,7 +137,10 @@ const actionHandlers: Record<string, ActionHandler> = {
|
||||
}
|
||||
|
||||
// Indexes in frontend are 1 based
|
||||
PlaybackService.startByIndex(eventIndex - 1);
|
||||
const success = PlaybackService.startByIndex(eventIndex - 1);
|
||||
if (!success) {
|
||||
throw new Error(`Event index not recognised or out of range ${eventIndex}`);
|
||||
}
|
||||
return { payload: 'success' };
|
||||
},
|
||||
startid: (payload) => {
|
||||
@@ -179,7 +182,6 @@ const actionHandlers: Record<string, ActionHandler> = {
|
||||
if (eventIndex <= 0) {
|
||||
throw new Error(`Event index out of range ${eventIndex}`);
|
||||
}
|
||||
|
||||
// Indexes in frontend are 1 based
|
||||
PlaybackService.loadByIndex(eventIndex - 1);
|
||||
return { payload: 'success' };
|
||||
@@ -200,7 +202,6 @@ const actionHandlers: Record<string, ActionHandler> = {
|
||||
return { payload: 'success' };
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a value of type number, converting if necessary
|
||||
* Otherwise throws
|
||||
|
||||
@@ -42,6 +42,8 @@ import { deleteFile } from '../utils/parserUtils.js';
|
||||
import { validateProjectFiles } from './ontimeController.validate.js';
|
||||
import { dbModel } from '../models/dataModel.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) => {
|
||||
@@ -668,3 +670,122 @@ export const deleteProjectFile: RequestHandler = async (req, res) => {
|
||||
res.status(500).send({ message: error.toString() });
|
||||
}
|
||||
};
|
||||
|
||||
// SHEET Functions
|
||||
/**
|
||||
* @description SETP-1 POST Client Secrect
|
||||
* @returns parsed result
|
||||
*/
|
||||
export async function uploadSheetClientFile(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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description STEP-1 GET Client Secrect status
|
||||
*/
|
||||
export const getClientSecrect = async (req, res) => {
|
||||
try {
|
||||
const clientSecrectExists = await sheet.testClientSecret();
|
||||
if (clientSecrectExists) {
|
||||
res.status(200).send();
|
||||
} else {
|
||||
res.status(500).send({ message: 'The Client ID does not exist' });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: error.toString() });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP-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 STEP-2 GET sheet authentication status
|
||||
*/
|
||||
export const getAuthentication = async (req, res) => {
|
||||
try {
|
||||
await sheet.testAuthentication();
|
||||
res.status(200).send();
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: error.toString() });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP-3 POST sheet id
|
||||
* @returns list of worksheets
|
||||
*/
|
||||
export const postId = async (req, res) => {
|
||||
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 STEP-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() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,3 +266,32 @@ export const validateProjectFiles = (projectFiles: { filename?: string; newFilen
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export const validateSheetid = [
|
||||
body('id').exists().isString(),
|
||||
(req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
export const validateWorksheet = [
|
||||
body('id').exists().isString(),
|
||||
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) => {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||
next();
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { copyFile } from 'fs/promises';
|
||||
import { pathToStartDemo, resolveDemoDirectory, resolveDemoPath } from '../setup.js';
|
||||
import { ensureDirectory } from '../utils/fileManagement.js';
|
||||
|
||||
/**
|
||||
* @description ensures directories exist and populates demo folder
|
||||
*/
|
||||
export const populateDemo = () => {
|
||||
ensureDirectory(resolveDemoDirectory);
|
||||
// even if demo exist we want to use startup demo
|
||||
try {
|
||||
Promise.all(
|
||||
resolveDemoPath.map((to, index) => {
|
||||
const from = pathToStartDemo[index];
|
||||
copyFile(from, to);
|
||||
}),
|
||||
);
|
||||
} catch (_) {
|
||||
/* we do not handle this */
|
||||
}
|
||||
};
|
||||
@@ -4,20 +4,15 @@ import { ensureDirectory } from '../utils/fileManagement.js';
|
||||
|
||||
/**
|
||||
* @description ensures directories exist and populates stylesheet
|
||||
* @return {string} - path to stylesheet file
|
||||
*/
|
||||
export const populateStyles = () => {
|
||||
const stylesInDisk = resolveStylesPath;
|
||||
ensureDirectory(resolveStylesDirectory);
|
||||
|
||||
// if stylesInDisk doesn't exist we want to use startup stylesheet
|
||||
if (!existsSync(stylesInDisk)) {
|
||||
// if styles doesn't exist we want to use startup stylesheet
|
||||
if (!existsSync(resolveStylesPath)) {
|
||||
try {
|
||||
copyFileSync(pathToStartStyles, stylesInDisk);
|
||||
copyFileSync(pathToStartStyles, resolveStylesPath);
|
||||
} catch (_) {
|
||||
/* we do not handle this */
|
||||
}
|
||||
}
|
||||
|
||||
return stylesInDisk;
|
||||
};
|
||||
|
||||
@@ -27,6 +27,13 @@ import {
|
||||
renameProjectFile,
|
||||
createProjectFile,
|
||||
deleteProjectFile,
|
||||
getAuthenticationUrl,
|
||||
uploadSheetClientFile as uploadClientSecret,
|
||||
pullSheet,
|
||||
pushSheet,
|
||||
postId,
|
||||
getAuthentication,
|
||||
getClientSecrect as getClientSecret,
|
||||
} from '../controllers/ontimeController.js';
|
||||
|
||||
import {
|
||||
@@ -42,6 +49,9 @@ import {
|
||||
validateLoadProjectFile,
|
||||
validateProjectRename,
|
||||
validateProjectCreate,
|
||||
validateSheetid,
|
||||
validateWorksheet,
|
||||
validateSheetOptions,
|
||||
} from '../controllers/ontimeController.validate.js';
|
||||
import { projectSanitiser } from '../controllers/projectController.validate.js';
|
||||
import { sanitizeProjectFilename } from '../utils/sanitizeProjectFilename.js';
|
||||
@@ -125,3 +135,23 @@ router.post('/project', validateProjectCreate, sanitizeProjectFilename, createPr
|
||||
|
||||
// create route between controller and '/ontime/project/:filename' endpoint
|
||||
router.delete('/project/:filename', sanitizeProjectFilename, deleteProjectFile);
|
||||
|
||||
// Google Sheet integration - Step 1
|
||||
router.post('/sheet/clientsecret', uploadFile, uploadClientSecret);
|
||||
router.get('/sheet/clientsecret', uploadFile, getClientSecret);
|
||||
|
||||
// Google Sheet integration - Step 1
|
||||
router.get('/sheet/authentication/url', getAuthenticationUrl);
|
||||
router.get('/sheet/authentication', getAuthentication);
|
||||
|
||||
// Google Sheet integration - Step 3
|
||||
router.post('/sheet/id', validateSheetid, postId);
|
||||
|
||||
// Google Sheet integration - Step 4
|
||||
router.post('/sheet/worksheet', validateWorksheet, postId);
|
||||
|
||||
// Google Sheet integration - Step 5
|
||||
router.post('/sheet/pull', validateSheetOptions, pullSheet);
|
||||
|
||||
// Google Sheet integration - Step 6
|
||||
router.post('/sheet-push', validateSheetOptions, pushSheet);
|
||||
|
||||
@@ -71,10 +71,16 @@ export const currentDirectory = dirname(__dirname);
|
||||
|
||||
const testDbStartDirectory = isTest ? '../' : getAppDataPath();
|
||||
export const externalsStartDirectory = isProduction ? getAppDataPath() : join(currentDirectory, 'external');
|
||||
//TODO: we only need one when they are all in the same folder
|
||||
export const resolveExternalsDirectory = join(isProduction ? getAppDataPath() : currentDirectory, 'external');
|
||||
|
||||
// path to public db
|
||||
export const resolveDbDirectory = join(testDbStartDirectory, isTest ? config.database.testdb : configDbDirectory);
|
||||
export const resolveDbPath = join(resolveDbDirectory, lastLoadedProject ? lastLoadedProject : config.database.filename);
|
||||
|
||||
// project files
|
||||
export const lastLoadedProjectConfigPath = join(getAppDataPath(), 'config.json');
|
||||
export const uploadsFolderPath = join(getAppDataPath(), 'uploads');
|
||||
|
||||
let lastLoadedProject;
|
||||
|
||||
try {
|
||||
@@ -88,19 +94,30 @@ try {
|
||||
|
||||
const configDbDirectory = lastLoadedProject ? 'uploads' : config.database.directory;
|
||||
|
||||
// path to public db
|
||||
export const resolveDbDirectory = join(testDbStartDirectory, isTest ? config.database.testdb : configDbDirectory);
|
||||
export const resolveDbPath = join(resolveDbDirectory, lastLoadedProject ? lastLoadedProject : config.database.filename);
|
||||
|
||||
export const pathToStartDb = isTest
|
||||
? join(currentDirectory, '../', config.database.testdb, config.database.filename)
|
||||
: join(currentDirectory, '/preloaded-db/', config.database.filename);
|
||||
|
||||
//TODO: move all static files to the external directory
|
||||
// path to public styles
|
||||
export const resolveStylesDirectory = join(externalsStartDirectory, config.styles.directory);
|
||||
export const resolveStylesPath = join(resolveStylesDirectory, config.styles.filename);
|
||||
|
||||
export const pathToStartStyles = join(currentDirectory, '/external/styles/', config.styles.filename);
|
||||
|
||||
// path to public demo
|
||||
export const resolveDemoDirectory = join(
|
||||
externalsStartDirectory,
|
||||
isProduction ? '/external/' : '', //move to external folde in production
|
||||
config.demo.directory,
|
||||
);
|
||||
export const resolveDemoPath = config.demo.filename.map((file) => {
|
||||
return join(resolveDemoDirectory, file);
|
||||
});
|
||||
|
||||
export const pathToStartDemo = config.demo.filename.map((file) => {
|
||||
return join(currentDirectory, '/external/demo/', file);
|
||||
});
|
||||
|
||||
// path to restore file
|
||||
export const resolveRestoreFile = join(getAppDataPath(), config.restoreFile);
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
import { millisToString } from 'ontime-utils';
|
||||
import { getA1Notation, cellRequestFromEvent, cellRequenstFromProjectData } from '../sheetUtils.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 = cellRequestFromEvent(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 = cellRequestFromEvent(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 = cellRequestFromEvent(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 = cellRequestFromEvent(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 = cellRequestFromEvent(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 = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||
expect(result1.updateCells.start.sheetId).toStrictEqual(1234);
|
||||
const result2 = cellRequestFromEvent(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');
|
||||
});
|
||||
});
|
||||
+227
-129
@@ -38,6 +38,7 @@ import {
|
||||
} from './parserFunctions.js';
|
||||
import { parseExcelDate } from './time.js';
|
||||
import { configService } from '../services/ConfigService.js';
|
||||
import { coerceBoolean } from './coerceType.js';
|
||||
|
||||
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
export const JSON_MIME = 'application/json';
|
||||
@@ -49,6 +50,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: '',
|
||||
@@ -107,146 +110,240 @@ 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, col };
|
||||
},
|
||||
[importMap.projectDescription]: (row: number, col: number) => {
|
||||
projectDescriptionNext = true;
|
||||
projectMetadata['description'] = { row, col };
|
||||
},
|
||||
[importMap.publicUrl]: (row: number, col: number) => {
|
||||
publicUrlNext = true;
|
||||
projectMetadata['publicUrl'] = { row, col };
|
||||
},
|
||||
[importMap.publicInfo]: (row: number, col: number) => {
|
||||
publicInfoNext = true;
|
||||
projectMetadata['publicInfo'] = { row, col };
|
||||
},
|
||||
[importMap.backstageUrl]: (row: number, col: number) => {
|
||||
backstageUrlNext = true;
|
||||
projectMetadata['backstageUrl'] = { row, col };
|
||||
},
|
||||
[importMap.backstageInfo]: (row: number, col: number) => {
|
||||
backstageInfoNext = true;
|
||||
projectMetadata['backstageInfo'] = { row, 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, col };
|
||||
},
|
||||
[importMap.timeEnd]: (row: number, col: number) => {
|
||||
timeEndIndex = col;
|
||||
rundownMetadata['timeEnd'] = { row, col };
|
||||
},
|
||||
[importMap.duration]: (row: number, col: number) => {
|
||||
durationIndex = col;
|
||||
rundownMetadata['duration'] = { row, 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, col };
|
||||
},
|
||||
[importMap.title]: (row: number, col: number) => {
|
||||
titleIndex = col;
|
||||
rundownMetadata['title'] = { row, col };
|
||||
},
|
||||
[importMap.presenter]: (row: number, col: number) => {
|
||||
presenterIndex = col;
|
||||
rundownMetadata['presenter'] = { row, col };
|
||||
},
|
||||
[importMap.subtitle]: (row: number, col: number) => {
|
||||
subtitleIndex = col;
|
||||
rundownMetadata['subtitle'] = { row, col };
|
||||
},
|
||||
[importMap.isPublic]: (row: number, col: number) => {
|
||||
isPublicIndex = col;
|
||||
rundownMetadata['isPublic'] = { row, col };
|
||||
},
|
||||
[importMap.skip]: (row: number, col: number) => {
|
||||
skipIndex = col;
|
||||
rundownMetadata['skip'] = { row, col };
|
||||
},
|
||||
[importMap.note]: (row: number, col: number) => {
|
||||
notesIndex = col;
|
||||
rundownMetadata['note'] = { row, col };
|
||||
},
|
||||
[importMap.colour]: (row: number, col: number) => {
|
||||
colourIndex = col;
|
||||
rundownMetadata['colour'] = { row, col };
|
||||
},
|
||||
|
||||
[importMap.endAction]: (index: number) => (endActionIndex = index),
|
||||
[importMap.timerType]: (index: number) => (timerTypeIndex = index),
|
||||
[importMap.endAction]: (row: number, col: number) => {
|
||||
endActionIndex = col;
|
||||
rundownMetadata['endAction'] = { row, col };
|
||||
},
|
||||
[importMap.timerType]: (row: number, col: number) => {
|
||||
timerTypeIndex = col;
|
||||
rundownMetadata['timerType'] = { row, col };
|
||||
},
|
||||
[importMap.timeWarning]: (row: number, col: number) => {
|
||||
timeWarningIndex = col;
|
||||
rundownMetadata['timeWarningIndex'] = { row, col };
|
||||
},
|
||||
[importMap.timeDanger]: (row: number, col: number) => {
|
||||
timeDangerIndex = col;
|
||||
rundownMetadata['timeDangerIndex'] = { row, 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),
|
||||
[importMap.timeWarning]: (index: number) => (timeWarningIndex = index),
|
||||
[importMap.timeDanger]: (index: number) => (timeDangerIndex = index),
|
||||
} as const;
|
||||
[importMap.user0]: (row: number, col: number) => {
|
||||
user0Index = col;
|
||||
rundownMetadata['user0'] = { row, col };
|
||||
},
|
||||
[importMap.user1]: (row: number, col: number) => {
|
||||
user1Index = col;
|
||||
rundownMetadata['user1'] = { row, col };
|
||||
},
|
||||
[importMap.user2]: (row: number, col: number) => {
|
||||
user2Index = col;
|
||||
rundownMetadata['user2'] = { row, col };
|
||||
},
|
||||
[importMap.user3]: (row: number, col: number) => {
|
||||
user3Index = col;
|
||||
rundownMetadata['user3'] = { row, col };
|
||||
},
|
||||
[importMap.user4]: (row: number, col: number) => {
|
||||
user4Index = col;
|
||||
rundownMetadata['user4'] = { row, col };
|
||||
},
|
||||
[importMap.user5]: (row: number, col: number) => {
|
||||
user5Index = col;
|
||||
rundownMetadata['user5'] = { row, col };
|
||||
},
|
||||
[importMap.user6]: (row: number, col: number) => {
|
||||
user6Index = col;
|
||||
rundownMetadata['user6'] = { row, col };
|
||||
},
|
||||
[importMap.user7]: (row: number, col: number) => {
|
||||
user7Index = col;
|
||||
rundownMetadata['user7'] = { row, col };
|
||||
},
|
||||
[importMap.user8]: (row: number, col: number) => {
|
||||
user8Index = col;
|
||||
rundownMetadata['user8'] = { row, col };
|
||||
},
|
||||
[importMap.user9]: (row: number, col: number) => {
|
||||
user9Index = col;
|
||||
rundownMetadata['user9'] = { row, 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 if (j === timeWarningIndex) {
|
||||
event.timeWarning = parseExcelDate(column);
|
||||
} else if (j === timeDangerIndex) {
|
||||
event.timeDanger = parseExcelDate(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 = column == 'x' ? true : coerceBoolean(column);
|
||||
} else if (j === skipIndex) {
|
||||
event.skip = column == 'x' ? true : coerceBoolean(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 === timeWarningIndex) {
|
||||
event.timeWarning = parseExcelDate(column);
|
||||
} else if (j === timeDangerIndex) {
|
||||
event.timeDanger = parseExcelDate(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,
|
||||
@@ -255,6 +352,8 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
||||
version: '2.0.0',
|
||||
},
|
||||
userFields: customUserFields,
|
||||
projectMetadata,
|
||||
rundownMetadata,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -403,7 +502,6 @@ export const fileHandler = async (file: string, options: ExcelImportOptions): Pr
|
||||
res.data = await parseJson(uploadedJson);
|
||||
|
||||
await configService.updateDatabaseConfig(fileName);
|
||||
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -328,4 +328,4 @@ export const parseUserFields = (data): UserFields => {
|
||||
}
|
||||
}
|
||||
return { ...newUserFields };
|
||||
};
|
||||
};
|
||||
@@ -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 cellRequestFromEvent(
|
||||
event: OntimeRundownEntry,
|
||||
index: number,
|
||||
worksheetId: number,
|
||||
metadata,
|
||||
): sheets_v4.Schema$Request {
|
||||
const returnRows: 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') {
|
||||
returnRows.push({});
|
||||
} else if (key === 'colour') {
|
||||
returnRows.push({
|
||||
userEnteredValue: { stringValue: event.colour },
|
||||
});
|
||||
} else if (typeof event[key] === 'number') {
|
||||
returnRows.push({
|
||||
userEnteredValue: { stringValue: millisToString(event[key], true) },
|
||||
});
|
||||
} else if (typeof event[key] === 'string') {
|
||||
returnRows.push({
|
||||
userEnteredValue: { stringValue: event[key] },
|
||||
});
|
||||
} else if (typeof event[key] === 'boolean') {
|
||||
returnRows.push({
|
||||
userEnteredValue: { stringValue: event[key] ? 'x' : '' },
|
||||
});
|
||||
} else {
|
||||
returnRows.push({});
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
updateCells: {
|
||||
start: {
|
||||
sheetId: worksheetId,
|
||||
rowIndex: index + tmp[0][1]['row'] + 1,
|
||||
columnIndex: titleCol,
|
||||
},
|
||||
fields: 'userEnteredValue',
|
||||
rows: [
|
||||
{
|
||||
values: returnRows,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 returnRows: 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') {
|
||||
returnRows.push({});
|
||||
} else {
|
||||
returnRows.push({
|
||||
values: [
|
||||
{
|
||||
userEnteredValue: { stringValue: projectData[key] },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
updateCells: {
|
||||
start: {
|
||||
sheetId: worksheetId,
|
||||
rowIndex: minRow,
|
||||
columnIndex: minCol,
|
||||
},
|
||||
fields: 'userEnteredValue',
|
||||
rows: returnRows,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
import { sheets, sheets_v4 } from '@googleapis/sheets';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { readFileSync } from 'fs';
|
||||
import { OAuth2Client } from 'google-auth-library';
|
||||
import http from 'http';
|
||||
import { DatabaseModel, LogOrigin } from 'ontime-types';
|
||||
import { join } from 'path';
|
||||
import { URL } from 'url';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||
import { getAppDataPath } from '../setup.js';
|
||||
import { ensureDirectory } from './fileManagement.js';
|
||||
import { cellRequestFromEvent, cellRequenstFromProjectData, getA1Notation } from './sheetUtils.js';
|
||||
import { parseExcel } from './parser.js';
|
||||
import { parseProject, parseRundown, parseUserFields } from './parserFunctions.js';
|
||||
import { ExcelImportMap } from 'ontime-utils';
|
||||
|
||||
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: string;
|
||||
private readonly clientSecretFile: string;
|
||||
private static clientSecret = null;
|
||||
private static authUrl: null | string = null;
|
||||
private authServerTimeout;
|
||||
|
||||
private readonly requiredClientKeys = [
|
||||
'client_id',
|
||||
'project_id',
|
||||
'auth_uri',
|
||||
'token_uri',
|
||||
'auth_provider_x509_cert_url',
|
||||
'client_secret',
|
||||
'redirect_uris',
|
||||
];
|
||||
|
||||
constructor() {
|
||||
const appDataPath = getAppDataPath();
|
||||
if (appDataPath === '') {
|
||||
throw new Error('Sheet: Could not resolve sheet folser');
|
||||
}
|
||||
this.sheetsFolder = join(appDataPath, 'sheets');
|
||||
this.clientSecretFile = join(this.sheetsFolder, 'client_secret.json');
|
||||
ensureDirectory(this.sheetsFolder);
|
||||
try {
|
||||
const secrets = JSON.parse(readFileSync(this.clientSecretFile, 'utf-8'));
|
||||
const isKeyMissing = this.requiredClientKeys.some((key) => !(key in secrets['installed']));
|
||||
if (!isKeyMissing) {
|
||||
Sheet.clientSecret = secrets;
|
||||
}
|
||||
} catch (_) {
|
||||
/* empty - it is ok thet there is no clientSecret */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description SETP 1 - 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;
|
||||
Sheet.clientSecret = null;
|
||||
|
||||
const isKeyMissing = this.requiredClientKeys.some((key) => !(key in secrets['installed']));
|
||||
if (isKeyMissing) {
|
||||
throw new Error('Client file is missing some keys');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description SETP 1 - test that the saved object is pressent
|
||||
*/
|
||||
testClientSecret() {
|
||||
return Sheet.clientSecret !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description SETP 2 - create server to interact with th OAuth2 request
|
||||
* @returns {Promise<string | null>} - returns url path serve on success
|
||||
* @throws
|
||||
*/
|
||||
async openAuthServer(): Promise<string | null> {
|
||||
//TODO: this only works on local networks
|
||||
|
||||
// if the server is allready running retun it
|
||||
if (Sheet.authUrl) {
|
||||
clearTimeout(this.authServerTimeout);
|
||||
this.authServerTimeout = setTimeout(
|
||||
() => {
|
||||
Sheet.authUrl = null;
|
||||
server.unref;
|
||||
},
|
||||
2 * 60 * 1000,
|
||||
);
|
||||
return Sheet.authUrl;
|
||||
}
|
||||
|
||||
// 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]);
|
||||
if (redirectUri.hostname !== 'localhost') {
|
||||
throw new Error('Sheet: Invalid redirect URI');
|
||||
}
|
||||
|
||||
// create an oAuth client to authorize the API call
|
||||
const client = new OAuth2Client({
|
||||
clientId: keys.client_id,
|
||||
clientSecret: keys.client_secret,
|
||||
});
|
||||
|
||||
// start the server that will recive the codes
|
||||
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,
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 access 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 readResponse = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
|
||||
spreadsheetId: id,
|
||||
valueRenderOption: 'FORMATTED_VALUE',
|
||||
majorDimension: 'ROWS',
|
||||
range: range,
|
||||
});
|
||||
if (readResponse.status === 200) {
|
||||
const { rundownMetadata, projectMetadata } = parseExcel(readResponse.data.values, options);
|
||||
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(cellRequestFromEvent(entry, index, worksheetId, rundownMetadata)),
|
||||
);
|
||||
|
||||
//update project data
|
||||
updateRundown.push(cellRequenstFromProjectData(projectData, worksheetId, projectMetadata));
|
||||
|
||||
const writeResponse = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.batchUpdate({
|
||||
spreadsheetId: id,
|
||||
requestBody: {
|
||||
includeSpreadsheetInResponse: false,
|
||||
responseRanges: [range],
|
||||
requests: updateRundown,
|
||||
},
|
||||
});
|
||||
|
||||
if (writeResponse.status === 200) {
|
||||
logger.info(LogOrigin.Server, `Sheet: write: ${writeResponse.statusText}`);
|
||||
} else {
|
||||
throw new Error(`Sheet: write failed: ${writeResponse.statusText}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Sheet: read failed: ${readResponse.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description STEP 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 googleResponse = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
|
||||
spreadsheetId: id,
|
||||
valueRenderOption: 'FORMATTED_VALUE',
|
||||
majorDimension: 'ROWS',
|
||||
range,
|
||||
});
|
||||
|
||||
if (googleResponse.status === 200) {
|
||||
res.data = {};
|
||||
const dataFromSheet = parseExcel(googleResponse.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`);
|
||||
}
|
||||
res.data.project = parseProject(dataFromSheet);
|
||||
res.data.userFields = parseUserFields(dataFromSheet);
|
||||
return res;
|
||||
} else {
|
||||
throw new Error(`Sheet: read failed: ${googleResponse.statusText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sheet = new Sheet();
|
||||
Reference in New Issue
Block a user