diff --git a/apps/client/src/common/api/ontimeApi.ts b/apps/client/src/common/api/ontimeApi.ts index fbc8cec2c..e4e69311a 100644 --- a/apps/client/src/common/api/ontimeApi.ts +++ b/apps/client/src/common/api/ontimeApi.ts @@ -3,6 +3,8 @@ import { Alias, DatabaseModel, GetInfo, + GoogleSheet, + GoogleSheetState, OntimeRundown, OSCSettings, OscSubscription, @@ -239,7 +241,7 @@ export const uploadSheetClientFile = async (file: File) => { .post(`${ontimeURL}/sheet-clientsecrect`, formData, { headers: { 'Content-Type': 'multipart/form-data', - } + }, }) .then((response) => response.data.id); }; @@ -273,3 +275,30 @@ export const postPushSheet = async (sheetId: string, worksheet: string, options? }; +/** + * @description HTTP request to retrieve google sheets settings + * @return {Promise} + */ +export async function getSheetSettings(): Promise { + 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 { + 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 { + const res = await axios.get(`${ontimeURL}/sheet-state`); + console.log(res.data) + return res.data; +} \ No newline at end of file diff --git a/apps/client/src/features/modals/sheets-modal/SheetsModal.tsx b/apps/client/src/features/modals/sheets-modal/SheetsModal.tsx index 60f7a0006..4719da72a 100644 --- a/apps/client/src/features/modals/sheets-modal/SheetsModal.tsx +++ b/apps/client/src/features/modals/sheets-modal/SheetsModal.tsx @@ -9,9 +9,13 @@ import { ModalFooter, ModalHeader, ModalOverlay, + useDisclosure, } from '@chakra-ui/react'; +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 { OntimeRundown, ProjectData, UserFields } from 'ontime-types'; +import { OntimeRundown, ProjectData, UserFields, GoogleSheetState } from 'ontime-types'; import { PROJECT_DATA, RUNDOWN, USERFIELDS } from '../../../common/api/apiConstants'; import { maybeAxiosError } from '../../../common/api/apiUtils'; @@ -22,6 +26,9 @@ import { postPreviewSheet, postPushSheet, uploadSheetClientFile, + getSheetSettings, + postSheetSettings, + getSheetstate, } from '../../../common/api/ontimeApi'; import { projectDataPlaceholder } from '../../../common/models/ProjectData'; import { userFieldsPlaceholder } from '../../../common/models/UserFields'; @@ -34,6 +41,7 @@ interface SheetsModalProps { export default function SheetsModal(props: SheetsModalProps) { const { isOpen, onClose } = props; + const fileInputRef = useRef(null); const [authState, setAuthState] = useState(true); @@ -41,14 +49,18 @@ export default function SheetsModal(props: SheetsModalProps) { const [userFields, setUserFields] = useState(null); const [project, setProject] = useState(null); + const [sheetState, setSheetState] = useState({ auth: false, id: false, worksheet: false }); + const queryClient = useQueryClient(); const sheetid = useRef(null); const worksheet = useRef(null); + const handleClose = () => { setRundown(null); setProject(null); setUserFields(null); + // setSheetState({ auth: false, id: false, worksheet: false }); onClose(); }; const handleClick = () => { @@ -64,19 +76,37 @@ export default function SheetsModal(props: SheetsModalProps) { } }; - //TODO: do smoething better here - getSheetsAuthStatus().then((data) => { - setAuthState(data); - }); + const _onChange = async () => { + setSheetState(await getSheetstate()); + }; - const handleAuthenticate = () => { - getSheetsAuthUrl().then((data) => { - console.log(data); - if (data != 'bad') { - window.open(data, '_blank', 'noreferrer'); + if (isOpen) { + //TODO: how to get this on modal open + 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 handlePullData = () => { postPreviewSheet(sheetid.current?.value ?? '', worksheet.current?.value ?? '').then((data) => { setProject(data.project); @@ -163,11 +193,12 @@ export default function SheetsModal(props: SheetsModalProps) { type='text' ref={sheetid} id='sheetid' - width='440px' + width='240px' size='sm' textAlign='right' variant='ontime-filled-on-light' /> + {sheetState.id ? : }
+ {sheetState.worksheet ? : } +
{!rundown && ( - )} {rundown && ( diff --git a/apps/server/src/controllers/ontimeController.ts b/apps/server/src/controllers/ontimeController.ts index 6c9026b6c..ad3358838 100644 --- a/apps/server/src/controllers/ontimeController.ts +++ b/apps/server/src/controllers/ontimeController.ts @@ -394,6 +394,29 @@ export async function previewExcel(req, res) { } } +/** + * Meant to create a new project file, it will clear only fields which are specific to a project + * @param req + * @param res + */ +export const postNew: RequestHandler = async (req, res) => { + try { + const newProjectData: ProjectData = { + title: req.body?.title ?? '', + description: req.body?.description ?? '', + publicUrl: req.body?.publicUrl ?? '', + publicInfo: req.body?.publicInfo ?? '', + backstageUrl: req.body?.backstageUrl ?? '', + backstageInfo: req.body?.backstageInfo ?? '', + }; + const newData = await DataProvider.setProjectData(newProjectData); + await deleteAllEvents(); + res.status(201).send(newData); + } catch (error) { + res.status(400).send({ message: error.toString() }); + } +}; + /** * downloads and parses an sheet * @returns parsed result @@ -424,7 +447,7 @@ export async function pushSheet(req, res) { * uploads Client secrets file * @returns parsed result */ -export async function sheetClientFile(req, res) { +export async function uploadGoogleSheetClientFile(req, res) { if (!req.file.path) { res.status(400).send({ message: 'File not found' }); return; @@ -432,8 +455,8 @@ export async function sheetClientFile(req, res) { try { const client = JSON.parse(fs.readFileSync(req.file.path as string, 'utf-8')); - await Sheet.saveClientSecrets(client); - res.status(200).send('OK'); + const auth = await Sheet.saveClientSecrets(client); + res.status(200).send(auth); } catch (error) { res.status(500).send({ message: error.toString() }); } @@ -463,24 +486,39 @@ export async function sheetAuthUrl(req, res) { } /** - * Meant to create a new project file, it will clear only fields which are specific to a project - * @param req - * @param res + * @description Get google sheet Settings + * @method GET */ -export const postNew: RequestHandler = async (req, res) => { +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 newProjectData: ProjectData = { - title: req.body?.title ?? '', - description: req.body?.description ?? '', - publicUrl: req.body?.publicUrl ?? '', - publicInfo: req.body?.publicInfo ?? '', - backstageUrl: req.body?.backstageUrl ?? '', - backstageInfo: req.body?.backstageInfo ?? '', + const newData = { + id: req.body.id, + worksheet: req.body.worksheet, }; - const newData = await DataProvider.setProjectData(newProjectData); - await deleteAllEvents(); - res.status(201).send(newData); + 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()); +}; diff --git a/apps/server/src/controllers/ontimeController.validate.ts b/apps/server/src/controllers/ontimeController.validate.ts index 48047c57b..8ac53a3f4 100644 --- a/apps/server/src/controllers/ontimeController.validate.ts +++ b/apps/server/src/controllers/ontimeController.validate.ts @@ -145,3 +145,13 @@ export const validateSheetPreview = [ 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(); + }, +]; diff --git a/apps/server/src/routes/ontimeRouter.ts b/apps/server/src/routes/ontimeRouter.ts index fe2caf131..86a743d90 100644 --- a/apps/server/src/routes/ontimeRouter.ts +++ b/apps/server/src/routes/ontimeRouter.ts @@ -21,13 +21,17 @@ import { previewExcel, sheetAuthUrl, sheetAuthState, - sheetClientFile, + uploadGoogleSheetClientFile, previewSheet, pushSheet, + getGoogleSheetSettings, + postGoogleSheetSettings, + getGoogleSheetState, } from '../controllers/ontimeController.js'; import { validateAliases, + validateGoogleSheetSettings, validateOSC, validateOscSubscription, validatePatchProjectFile, @@ -95,7 +99,7 @@ router.post('/osc-subscriptions', validateOscSubscription, postOscSubscriptions) router.post('/new', projectSanitiser, postNew); // create route between controller and '/ontime/sheet-client' endpoint -router.post('/sheet-clientsecrect', uploadFile, sheetClientFile); +router.post('/sheet-clientsecrect', uploadFile, uploadGoogleSheetClientFile); // create route between controller and '/ontime/sheet-authstatus' endpoint router.get('/sheet-authstatus', sheetAuthState); @@ -107,4 +111,13 @@ router.get('/sheet-authurl', sheetAuthUrl); router.post('/sheet-preview', validateSheetPreview, previewSheet); // create route between controller and '/ontime/preview-sheet' endpoint -router.post('/sheet-push', pushSheet); \ No newline at end of file +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); \ No newline at end of file diff --git a/apps/server/src/utils/sheetsAuth.ts b/apps/server/src/utils/sheetsAuth.ts index b0c8cc624..18ea09f5e 100644 --- a/apps/server/src/utils/sheetsAuth.ts +++ b/apps/server/src/utils/sheetsAuth.ts @@ -11,6 +11,7 @@ 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'; type ResponseOK = { data: Partial; @@ -24,6 +25,27 @@ class sheet { private readonly token = this.sheetsFolder + '/token.json'; private static authUrl: null | string = null; + public async getSheetState(): Promise { + const ret: GoogleSheetState = { + auth: false, + id: false, + worksheet: false, + }; + ret.auth = await this.authorized(); + if (ret.auth) { + const settings = DataProvider.getGoogleSheet(); + const x = await this.exist(settings.id, settings.worksheet); + if (x === true) { + ret.id = true; + } else if (x !== false) { + ret.id = true; + ret.worksheet = true; + } + } + logger.info(LogOrigin.Server, `Sheet State: ${ret}`); + return ret; + } + /** * checks the authorized state * @returns {Promise} @@ -42,10 +64,13 @@ class sheet { * test existance of sheet and workssheet and get is index * @param {string} sheetId - https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0 * @param {string} worksheet - the name of the worksheet containg ontime data - * @returns {Promise} - false if not found | id of worksheet + * @returns {Promise} - false if not found | true if sheetId existes | id of worksheet and rage of worksheet * @throws */ - public async exist(sheetId: string, worksheet: string): Promise { + public async exist( + sheetId: string, + worksheet: string, + ): Promise { const spreadsheets = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.get({ spreadsheetId: sheetId, }); @@ -58,6 +83,8 @@ class sheet { w.properties.gridProperties.columnCount, ); return { worksheetId: w.properties.sheetId, range: worksheet + '!A1:' + endCell }; + } else { + return true; } } return false; @@ -73,6 +100,8 @@ class sheet { const sheetInfo = await this.exist(sheetId, worksheet); if (!sheetInfo) { throw new Error(`Sheet not dose not exits`); + } else if (sheetInfo === true) { + throw new Error(`Worksheet not dose not exits`); } if (!isExcelImportMap(options)) { @@ -176,6 +205,8 @@ class sheet { const sheetInfo = await this.exist(sheetId, worksheet); if (!sheetInfo) { throw new Error(`Sheet not dose not exits`); + } else if (sheetInfo === true) { + throw new Error(`Worksheet not dose not exits`); } const rq = await sheets({ version: 'v4', auth: sheet.client }).spreadsheets.values.get({ @@ -203,7 +234,7 @@ class sheet { * saves Object to appdata path as client_secret.json * @param {} secrets */ - public async saveClientSecrets(secrets) { + public async saveClientSecrets(secrets): Promise { ensureDirectory(this.sheetsFolder); logger.info(LogOrigin.Server, 'Sheets: got new client_secret'); //TODO: test that this is actualy a client file? @@ -211,6 +242,7 @@ class sheet { sheet.client = null; sheet.authUrl = null; await writeFile(this.client_secret, JSON.stringify(secrets), 'utf-8'); + return await this.getSheetState(); } /** diff --git a/packages/types/src/definitions/core/GoogleSheet.type.ts b/packages/types/src/definitions/core/GoogleSheet.type.ts index 85a11a315..717c205ad 100644 --- a/packages/types/src/definitions/core/GoogleSheet.type.ts +++ b/packages/types/src/definitions/core/GoogleSheet.type.ts @@ -2,3 +2,9 @@ export type GoogleSheet = { worksheet: string; id: string; }; + +export type GoogleSheetState = { + auth: boolean; + id: boolean; + worksheet: boolean; +}; \ No newline at end of file diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 699a2a9fc..21883ca28 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -34,7 +34,7 @@ export type { OSCSettings, OscSubscription, OscSubscriptionOptions } from './def // ---> HTTP // ---> Google Sheet -export type { GoogleSheet } from './definitions/core/GoogleSheet.type.js'; +export type { GoogleSheet, GoogleSheetState } from './definitions/core/GoogleSheet.type.js'; // SERVER RESPONSES export type { NetworkInterface, GetInfo } from './api/ontime-controller/BackendResponse.type.js';