Files
ontime/apps/server/src/services/sheet-service/sheetUtils.ts
T

217 lines
6.4 KiB
TypeScript

import {
OntimeEntry,
RGBColour,
isOntimeDelay,
OntimeEntryCommonKeys,
isOntimeGroup,
isOntimeMilestone,
} from 'ontime-types';
import { cssOrHexToColour, isLightColour, millisToString, mixColours } from 'ontime-utils';
import type { sheets_v4 } from '@googleapis/sheets';
import { is } from '../../utils/is.js';
export type ClientSecret = {
installed: {
client_id: string;
auth_uri: string;
token_uri: string;
auth_provider_x509_cert_url: string;
client_secret: string;
};
};
/**
* Guard validates a given client secrets file
* @param clientSecret
* @throws
*/
export function isClientSecret(clientSecret: object): clientSecret is ClientSecret {
if (!('installed' in clientSecret)) {
return false;
}
const { installed } = clientSecret;
if (!is.object(installed)) {
return false;
}
// we expect client secret file to contain the following keys
return is.objectWithKeys(installed, [
'client_id',
'auth_uri',
'token_uri',
'token_uri',
'auth_provider_x509_cert_url',
'client_secret',
]);
}
/**
*
* @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 {OntimeEntry} entry
* @param {number} index - index of the event
* @param {number} worksheetId
* @param {object} metadata - object with all the cell positions of the title of each attribute
* @returns {sheets_v4.Schema} - list of update requests
*/
export function cellRequestFromEvent(
entry: OntimeEntry,
index: number,
worksheetId: number,
metadata: object,
): sheets_v4.Schema$Request {
const rowData = Object.entries(metadata) // check what headings are available in the sheet
.filter(([_, value]) => value !== undefined) // drop anything that is undefined
.sort(([_a, a], [_b, b]) => a['col'] - b['col']) as [
OntimeEntryCommonKeys | 'blank',
{ col: number; row: number },
][]; // sort the array by the column index
// inset blank data is there is spacing between relevant ontime columns
for (const [index, e] of rowData.entries()) {
if (index === 0) continue;
const prevCol = rowData[index - 1][1].col;
const thisCol = e[1].col;
const diff = thisCol - prevCol;
if (diff > 1) {
const fillArr = new Array<(typeof rowData)[0]>(1).fill(['blank', { row: e[1].row, col: prevCol + 1 }]);
rowData.splice(index, 0, ...fillArr);
}
}
const colours = 'colour' in entry ? getAccessibleColour(entry.colour) : undefined;
const cellColor: sheets_v4.Schema$CellData = !colours
? {}
: {
userEnteredFormat: {
backgroundColor: toSheetColourLevel(colours.background),
textFormat: {
foregroundColor: toSheetColourLevel(colours.text),
},
borders: {
bottom: {
style: 'SOLID',
color: toSheetColourLevel(colours.border),
},
},
},
};
const returnRows: sheets_v4.Schema$CellData[] = rowData.map(([key, _]) => {
return { ...getCellData(key, entry), ...cellColor };
});
const headerLocation = rowData[0][1];
return {
updateCells: {
start: {
sheetId: worksheetId,
rowIndex: index + headerLocation.row + 1,
columnIndex: headerLocation.col,
},
fields: 'userEnteredValue,userEnteredFormat',
rows: [
{
values: returnRows,
},
],
},
};
}
function getCellData(key: OntimeEntryCommonKeys | 'blank', entry: OntimeEntry) {
if (isOntimeDelay(entry) || key === 'blank') {
return {};
}
// we need to remap the event type to timer type in the case of groups and milestones
if (key === 'timerType') {
if (isOntimeGroup(entry))
return { userEnteredValue: { stringValue: entry.id.startsWith('group-end') ? 'group-end' : 'group' } };
if (isOntimeMilestone(entry)) return { userEnteredValue: { stringValue: 'milestone' } };
return { userEnteredValue: { stringValue: entry.timerType } };
}
// all other data is not relevant for the group end entry
if (entry.id.startsWith('group-end')) {
return {};
}
// we need to flatten the milestones
if (key.startsWith('custom')) {
const customKey = key.split(':')[1];
return { userEnteredValue: { stringValue: entry.custom[customKey] } };
}
// typescript cannot guarantee that the key exists for every entry
// so we check for the key existence and assert the type
if (!(key in entry)) return {};
const value = entry[key as keyof OntimeEntry];
if (typeof value === 'number') {
return { userEnteredValue: { stringValue: millisToString(value) } };
}
if (typeof value === 'string') {
return { userEnteredValue: { stringValue: value } };
}
if (typeof value === 'boolean') {
return { userEnteredValue: { boolValue: value } };
}
}
type googleSheetCellColour = {
background: RGBColour;
text: RGBColour;
border: RGBColour;
};
function getAccessibleColour(bgColour?: string): googleSheetCellColour | undefined {
if (!bgColour) return undefined;
const background = cssOrHexToColour(bgColour);
if (!background) return undefined;
const text = isLightColour(background) ? BLACK : WHITE;
const border = mixColours(background, text, 0.2);
return { background, text, border };
}
const BLACK: RGBColour = { red: 0, green: 0, blue: 0, alpha: 1 };
const WHITE: RGBColour = { red: 255, green: 255, blue: 255, alpha: 0.98 };
// sheets use color values from 0 to 1
function toSheetColourLevel(colour: RGBColour): RGBColour {
return { red: colour.red / 255, green: colour.green / 255, blue: colour.blue / 255, alpha: colour.alpha };
}