import type { SpreadsheetPreviewResponse, SpreadsheetWorksheetMetadata, SpreadsheetWorksheetOptions, } from 'ontime-types'; import { getErrorMessage, ImportMap } from 'ontime-utils'; import { ChangeEvent, useCallback, useRef, useState } from 'react'; import { IoCloudOutline, IoDownloadOutline } from 'react-icons/io5'; import { getWorksheetMetadata as getExcelWorksheetMetadata, importRundownPreview as importExcelPreview, upload as uploadExcel, } from '../../../../../common/api/excel'; import { getWorksheetMetadata as getGoogleWorksheetMetadata, previewRundown as previewGoogleSheet, uploadRundown, } from '../../../../../common/api/sheets'; import Button from '../../../../../common/components/buttons/Button'; import Info from '../../../../../common/components/info/Info'; import ExternalLink from '../../../../../common/components/link/external-link/ExternalLink'; import Modal from '../../../../../common/components/modal/Modal'; import useRundown from '../../../../../common/hooks-query/useRundown'; import { validateExcelImport } from '../../../../../common/utils/uploadUtils'; import * as Panel from '../../../panel-utils/PanelUtils'; import GSheetSetup from './GSheetSetup'; import SheetImportEditor from './sheet-import/SheetImportEditor'; import useSpreadsheetImport from './useSpreadsheetImport'; import style from './SourcesPanel.module.scss'; const googleSheetDocsUrl = 'https://docs.getontime.no/features/import-spreadsheet-gsheet/'; type ActiveSource = | { kind: 'excel'; worksheetNames: string[]; initialWorksheetMetadata: SpreadsheetWorksheetMetadata | null; closedByUser: boolean; } | { kind: 'gsheet'; sheetId: string; worksheetNames: string[]; initialWorksheetMetadata: SpreadsheetWorksheetMetadata | null; title: string; closedByUser: boolean; }; export default function SourcesPanel() { const [importFlow, setImportFlow] = useState<'none' | 'excel' | 'gsheet' | 'finished'>('none'); const [error, setError] = useState(''); const [hasFile, setHasFile] = useState<'none' | 'loading' | 'done'>('none'); const [activeSource, setActiveSource] = useState(null); const { data: currentRundown } = useRundown(); const { importRundown } = useSpreadsheetImport(); const fileInputRef = useRef(null); const handleFile = async (event: ChangeEvent) => { const fileToUpload = event.target.files?.[0]; if (!fileToUpload) { setActiveSource(null); setHasFile('none'); return; } try { setHasFile('loading'); setError(''); validateExcelImport(fileToUpload); const worksheetOptions = await uploadExcel(fileToUpload); setActiveSource({ kind: 'excel', worksheetNames: worksheetOptions.worksheets, initialWorksheetMetadata: worksheetOptions.metadata, closedByUser: false, }); setImportFlow('excel'); setHasFile('done'); } catch (error) { const errorMessage = getErrorMessage(error); setError(`Error uploading file: ${errorMessage}`); setActiveSource(null); setHasFile('none'); } }; const handleUpload = () => { fileInputRef.current?.click(); }; const resetFlow = () => { setImportFlow('none'); setHasFile('none'); setActiveSource(null); setError(''); }; const openGSheetFlow = () => { setError(''); setActiveSource(null); setImportFlow('gsheet'); }; const cancelGSheetFlow = () => { resetFlow(); }; const cancelImportFlow = () => { if (activeSource && activeSource.kind === 'gsheet') { // Return to GSheetSetup so the user can change the sheet ID or revoke auth setActiveSource({ ...activeSource, closedByUser: true }); setError(''); } else { resetFlow(); } }; const handleFinished = () => { setImportFlow('finished'); setHasFile('none'); setActiveSource(null); setError(''); }; const handleApplyImport = async (preview: SpreadsheetPreviewResponse) => { if (!currentRundown) { throw new Error('No current rundown loaded'); } await importRundown( { [currentRundown.id]: { ...preview.rundown, id: currentRundown.id, title: currentRundown.title, }, }, preview.customFields, ); handleFinished(); }; const loadWorksheetMetadata = useCallback( (worksheet: string) => { if (!activeSource) { throw new Error('No spreadsheet source loaded'); } return activeSource.kind === 'excel' ? getExcelWorksheetMetadata(worksheet) : getGoogleWorksheetMetadata(activeSource.sheetId, worksheet); }, [activeSource], ); const previewImport = useCallback( (importMap: ImportMap): Promise => { if (!activeSource) { throw new Error('No spreadsheet source loaded'); } return activeSource.kind === 'excel' ? importExcelPreview(importMap) : previewGoogleSheet(activeSource.sheetId, importMap); }, [activeSource], ); const exportToGoogleSheet = useCallback( (importMap: ImportMap): Promise => { if (!activeSource || activeSource.kind !== 'gsheet') { throw new Error('Google Sheet source not available'); } return uploadRundown(activeSource.sheetId, importMap); }, [activeSource], ); const handleSheetLoaded = useCallback((sheetId: string, worksheetOptions: SpreadsheetWorksheetOptions) => { setActiveSource({ kind: 'gsheet', sheetId, worksheetNames: worksheetOptions.worksheets, initialWorksheetMetadata: worksheetOptions.metadata, title: worksheetOptions.title ?? '', closedByUser: false, }); }, []); const closedByUser = activeSource?.closedByUser ?? false; const isGSheetFlow = importFlow === 'gsheet'; const showInput = importFlow === 'none'; const showCompleted = importFlow === 'finished'; const showImportWorkspace = activeSource !== null && !closedByUser; const importModalTitle = (() => { if (!activeSource) return ''; if (activeSource.kind === 'excel') return 'Import spreadsheet'; return activeSource.title ? `Sync: ${activeSource.title}` : 'Synchronize with Google Sheet'; })(); const sourceKey = (() => { if (!activeSource) return null; if (activeSource.kind === 'excel') return 'excel'; return `gsheet:${activeSource.sheetId}`; })(); return ( Synchronize your rundown with an external source {error && {error}} {showInput && (
Choose between a quick file import or a live Google Sheet connection. Google Sheets sync needs a client secret and a one-time device authentication before you can load a sheet by ID. Read setup guide

Import spreadsheet

Bring in a one-off spreadsheet, review the mapping, and apply the data to the current rundown.

Accepts `.xlsx` files

Synchronize with Google

Connect a Google account once, then load any sheet by ID and keep the import flow inside Ontime.

Requires Google OAuth client credentials
)} {showCompleted && (
Import complete
Spreadsheet data applied.
You can close this flow or start another import.
)} {isGSheetFlow && ( )} } />
); }