mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-15 04:13:47 +00:00
Sheets settings (#774)
* wip: migrate sheet settings --------- Co-authored-by: arc-alex <ac@omnivox.dk>
This commit is contained in:
@@ -7,6 +7,7 @@ import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
|
||||
import LogPanel from './panel/log-panel/LogPanel';
|
||||
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||
import ProjectSettingsPanel from './panel/project-settings-panel/ProjectSettingsPanel';
|
||||
import SourcesPanel from './panel/sources-panel/SourcesPanel';
|
||||
import PanelContent from './panel-content/PanelContent';
|
||||
import PanelList from './panel-list/PanelList';
|
||||
import { useSettingsStore } from './settingsStore';
|
||||
@@ -28,6 +29,7 @@ export default function AppSettings() {
|
||||
<PanelList />
|
||||
<PanelContent onClose={closeSettings}>
|
||||
{selectedPanel === 'project' && <ProjectPanel />}
|
||||
{selectedPanel === 'sources' && <SourcesPanel />}
|
||||
{selectedPanel === 'integrations' && <IntegrationsPanel />}
|
||||
{selectedPanel === 'project_settings' && <ProjectSettingsPanel />}
|
||||
{selectedPanel === 'about' && <AboutPanel />}
|
||||
|
||||
@@ -17,11 +17,13 @@ $inner-padding: 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.25rem;
|
||||
padding-left: $inner-padding;
|
||||
font-size: 1.375rem;
|
||||
padding: 0 2rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: $gray-300;
|
||||
}
|
||||
|
||||
.section {
|
||||
@@ -49,7 +51,7 @@ $inner-padding: 1rem;
|
||||
|
||||
.pad {
|
||||
padding: 0 1rem;
|
||||
max-height: 500px;
|
||||
max-height: 550px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
@@ -80,7 +82,7 @@ $inner-padding: 1rem;
|
||||
}
|
||||
|
||||
.listGroup {
|
||||
padding: 0.5rem 1rem 0 1rem;
|
||||
padding: 1rem 2rem 0 2rem;
|
||||
|
||||
> li:not(:last-child) {
|
||||
border-bottom: 1px solid $white-10;
|
||||
|
||||
@@ -17,18 +17,14 @@ export default function AboutPanel() {
|
||||
</Panel.Paragraph>
|
||||
</Panel.Section>
|
||||
<Panel.Section>
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>Links</Panel.SubHeader>
|
||||
<ExternalLink href={gitbookUrl}>Read the docs over at GitBook</ExternalLink>
|
||||
<ExternalLink href={githubUrl}>Follow the project on GitHub</ExternalLink>
|
||||
</Panel.Card>
|
||||
<Panel.SubHeader>Links</Panel.SubHeader>
|
||||
<ExternalLink href={gitbookUrl}>Read the docs over at GitBook</ExternalLink>
|
||||
<ExternalLink href={githubUrl}>Follow the project on GitHub</ExternalLink>
|
||||
</Panel.Section>
|
||||
<Panel.Section>
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>Current version</Panel.SubHeader>
|
||||
<Panel.Paragraph>{`You are currently using Ontime ${version}`}</Panel.Paragraph>
|
||||
<CheckUpdatesButton version={version} />
|
||||
</Panel.Card>
|
||||
<Panel.SubHeader>Current version</Panel.SubHeader>
|
||||
<Panel.Paragraph>{`You are currently using Ontime ${version}`}</Panel.Paragraph>
|
||||
<CheckUpdatesButton version={version} />
|
||||
</Panel.Section>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function CheckUpdatesButton(props: CheckUpdatesButtonProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={versionCheck} variant='ontime-filled' isLoading={isFetching} isDisabled={disableButton}>
|
||||
<Button onClick={versionCheck} variant='ontime-filled' isLoading={isFetching} isDisabled={disableButton} size='sm'>
|
||||
Check for updates
|
||||
</Button>
|
||||
<ResolveUpdateMessage updateMessage={updateMessage} />
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fitContents {
|
||||
width: max-content !important; /* override chakra */
|
||||
.fitContents.fitContents {
|
||||
width: max-content; /* override chakra */
|
||||
}
|
||||
|
||||
.flex {
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export default function IntegrationsPanel() {
|
||||
<Alert status='info' variant='ontime-on-dark-info'>
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
Integrations allow Ontime to receive commands or send its data to other systems in your workflow. <br />{' '}
|
||||
Integrations allow Ontime to receive commands or send its data to other systems in your workflow. <br />
|
||||
<br />
|
||||
Currently supported protocols are OSC (Open Sound Control), HTTP and Websockets. <br />
|
||||
WebSockets are used for Ontime and cannot be configured independently. <br />
|
||||
|
||||
@@ -97,7 +97,11 @@ export default function OscIntegrations() {
|
||||
</div>
|
||||
</Panel.SubHeader>
|
||||
|
||||
<Panel.Divider />
|
||||
|
||||
|
||||
<Panel.Section as='form' id='osc-form' onSubmit={handleSubmit(onSubmit)} onKeyDown={preventEscape}>
|
||||
<Panel.Title>OSC Settings</Panel.Title>
|
||||
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||
<Panel.ListGroup>
|
||||
<Panel.ListItem>
|
||||
|
||||
@@ -19,9 +19,14 @@ export default function ProjectPanel() {
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>
|
||||
Manage projects
|
||||
<Button variant='ontime-filled' onClick={handleToggleCreate}>
|
||||
New
|
||||
</Button>
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<Button variant='ontime-subtle' onClick={handleToggleCreate} size='sm'>
|
||||
Import
|
||||
</Button>
|
||||
<Button variant='ontime-subtle' onClick={handleToggleCreate} size='sm'>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.SubHeader>
|
||||
<ProjectList onToggleCreate={handleToggleCreate} isCreatingProject={isCreatingProject} />
|
||||
</Panel.Card>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Alert, AlertDescription, AlertIcon } from '@chakra-ui/react';
|
||||
|
||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||
const googleSheetDocsUrl = 'https://ontime.gitbook.io/v2/features/google-sheet';
|
||||
|
||||
export default function GSheetInfo() {
|
||||
return (
|
||||
<Alert status='info' variant='ontime-on-dark-info'>
|
||||
<AlertIcon />
|
||||
<AlertDescription>
|
||||
Ontime allows you to synchronize your rundown with a Google Sheet.
|
||||
<br />
|
||||
<br />
|
||||
To enable this feature, you will need to generate tokens in your Google account and provide them to Ontime.
|
||||
<br />
|
||||
Once set up, you will be able to synchronize data between Ontime and your Google Sheet. <br />
|
||||
<ExternalLink href={googleSheetDocsUrl}>See the docs</ExternalLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useRef } from 'react';
|
||||
import { Button, Input, Select } from '@chakra-ui/react';
|
||||
import { IoCheckmark } from '@react-icons/all-files/io5/IoCheckmark';
|
||||
import { IoCloudDownloadOutline } from '@react-icons/all-files/io5/IoCloudDownloadOutline';
|
||||
import { IoShieldCheckmarkOutline } from '@react-icons/all-files/io5/IoShieldCheckmarkOutline';
|
||||
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import useGoogleSheet from './useGoogleSheet';
|
||||
import { useSheetStore } from './useSheetStore';
|
||||
|
||||
import style from './SourcesPanel.module.scss';
|
||||
|
||||
interface GSheetSetupProps {
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
export default function GSheetSetup({ cancel }: GSheetSetupProps) {
|
||||
const { handleClientSecret, handleAuthenticate, handleConnect } = useGoogleSheet();
|
||||
|
||||
const sheetIdInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const stepData = useSheetStore((state) => state.stepData);
|
||||
const reset = useSheetStore((state) => state.reset);
|
||||
|
||||
const sheetId = useSheetStore((state) => state.sheetId);
|
||||
const worksheetOptions = useSheetStore((state) => state.worksheetOptions) ?? [];
|
||||
|
||||
const setWorksheet = useSheetStore((state) => state.setWorksheet);
|
||||
const setSheetId = useSheetStore((state) => state.setSheetId);
|
||||
|
||||
const worksheetIdInputRef = useRef<HTMLSelectElement>(null);
|
||||
|
||||
// user cancels the flow
|
||||
const onCancel = () => {
|
||||
reset();
|
||||
cancel();
|
||||
};
|
||||
|
||||
// connect to the accoutn with the given sheet ID
|
||||
const connectToId = () => {
|
||||
const sheetId = sheetIdInputRef.current?.value;
|
||||
if (!sheetId) return;
|
||||
|
||||
handleConnect(sheetId);
|
||||
};
|
||||
|
||||
// adds the user input sheet ID to the store
|
||||
const addSheetId = () => {
|
||||
const sheetId = sheetIdInputRef.current?.value;
|
||||
console.log('adding', sheetId);
|
||||
if (!sheetId) return;
|
||||
setSheetId(sheetId);
|
||||
};
|
||||
|
||||
// adds the selected worksheet to the store
|
||||
const addWorksheetSheetId = () => {
|
||||
const worksheetId = worksheetIdInputRef.current?.value;
|
||||
if (!worksheetId) return;
|
||||
setWorksheet(worksheetId);
|
||||
};
|
||||
|
||||
const canAuthenticate = stepData.authenticate.available;
|
||||
const canConnect = stepData.authenticate.available && sheetId;
|
||||
|
||||
return (
|
||||
<Panel.Section>
|
||||
<Panel.Title>
|
||||
Sync with Google Sheet (experimental)
|
||||
<Button variant='ontime-subtle' size='sm' onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Panel.Title>
|
||||
<Panel.ListGroup>
|
||||
<div className={style.buttonRow}>
|
||||
<div className={style.inputContainer}>
|
||||
<Input type='file' onChange={handleClientSecret} accept='.json' size='sm' variant='ontime-filled' />
|
||||
</div>
|
||||
<Button
|
||||
variant='ontime-subtle'
|
||||
size='sm'
|
||||
onClick={handleAuthenticate}
|
||||
leftIcon={<IoShieldCheckmarkOutline />}
|
||||
isDisabled={!canAuthenticate}
|
||||
>
|
||||
Authenticate
|
||||
</Button>
|
||||
</div>
|
||||
<Panel.Error>{stepData.clientSecret.error}</Panel.Error>
|
||||
</Panel.ListGroup>
|
||||
|
||||
<Panel.ListGroup>
|
||||
<Panel.Error>{stepData.sheetId.error}</Panel.Error>
|
||||
<div className={style.buttonRow}>
|
||||
<div className={style.inputContainer}>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
autoComplete='off'
|
||||
isDisabled={!stepData.sheetId.available}
|
||||
placeholder='Enter Sheet ID'
|
||||
onBlur={addSheetId}
|
||||
onSubmit={addSheetId}
|
||||
ref={sheetIdInputRef}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant='ontime-subtle'
|
||||
size='sm'
|
||||
onClick={connectToId}
|
||||
isDisabled={!canConnect}
|
||||
leftIcon={<IoCheckmark />}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.ListGroup>
|
||||
|
||||
<Panel.ListGroup>
|
||||
<div className={style.buttonRow}>
|
||||
<div className={style.inputContainer}>
|
||||
<Select
|
||||
size='sm'
|
||||
variant='ontime'
|
||||
isDisabled={!stepData.worksheet.available}
|
||||
placeholder='Select worksheet'
|
||||
ref={worksheetIdInputRef}
|
||||
>
|
||||
{worksheetOptions.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
onClick={addWorksheetSheetId}
|
||||
isDisabled={!stepData.worksheet.available}
|
||||
leftIcon={<IoCloudDownloadOutline />}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
<Panel.Error>{stepData.worksheet.error}</Panel.Error>
|
||||
<Panel.Error>{stepData.pullPush.error}</Panel.Error>
|
||||
</Panel.ListGroup>
|
||||
</Panel.Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
|
||||
import ExcelFileOptions from '../../../modals/upload-modal/upload-options/ExcelFileOptions';
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import useGoogleSheet from './useGoogleSheet';
|
||||
import { useSheetStore } from './useSheetStore';
|
||||
|
||||
import style from './SourcesPanel.module.scss';
|
||||
|
||||
export default function ImportMap() {
|
||||
const { handleImportPreview, handleExport } = useGoogleSheet();
|
||||
|
||||
const sheetId = useSheetStore((state) => state.sheetId);
|
||||
const worksheetId = useSheetStore((state) => state.worksheet);
|
||||
const importOptions = useSheetStore((state) => state.excelFileOptions);
|
||||
const patchImportOptions = useSheetStore((state) => state.patchExcelFileOptions);
|
||||
const stepData = useSheetStore((state) => state.stepData);
|
||||
|
||||
const exportRundown = () => {
|
||||
if (!worksheetId || !sheetId) return;
|
||||
handleExport(sheetId, worksheetId, importOptions);
|
||||
};
|
||||
|
||||
const importPreviewRundown = () => {
|
||||
if (!worksheetId || !sheetId) return;
|
||||
handleImportPreview(sheetId, worksheetId, importOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<Panel.Section>
|
||||
<Panel.Title>Import options</Panel.Title>
|
||||
<ExcelFileOptions importOptions={importOptions} updateOptions={patchImportOptions} />
|
||||
<Panel.Error>{stepData.worksheet.error}</Panel.Error>
|
||||
<div className={style.buttonRow}>
|
||||
<Button variant='ontime-filled' size='sm' onClick={exportRundown}>
|
||||
Export
|
||||
</Button>
|
||||
<Button variant='ontime-filled' size='sm' onClick={importPreviewRundown}>
|
||||
Import preview
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { OntimeRundown, UserFields } from 'ontime-types';
|
||||
|
||||
import PreviewExcel from '../../../modals/upload-modal/preview/PreviewExcel';
|
||||
|
||||
import useGoogleSheet from './useGoogleSheet';
|
||||
import { useSheetStore } from './useSheetStore';
|
||||
|
||||
import style from './SourcesPanel.module.scss';
|
||||
|
||||
interface ImportReviewProps {
|
||||
rundown: OntimeRundown;
|
||||
userFields: UserFields;
|
||||
}
|
||||
|
||||
export default function ImportReview({ rundown, userFields }: ImportReviewProps) {
|
||||
const { handleImport } = useGoogleSheet();
|
||||
const resetPreview = useSheetStore((state) => state.resetPreview);
|
||||
|
||||
const applyImport = () => {
|
||||
handleImport(rundown, userFields);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreviewExcel rundown={rundown} userFields={userFields} />
|
||||
<div className={style.buttonRow}>
|
||||
<Button onClick={resetPreview} variant='ontime-ghosted' size='sm'>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={applyImport} variant='ontime-filled' size='sm'>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.uploadSection {
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2rem;
|
||||
padding: 3rem 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $gray-1350;
|
||||
border: 1px solid $white-10;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.buttonRow {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { Button, Input } from '@chakra-ui/react';
|
||||
import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
|
||||
import { IoDownloadOutline } from '@react-icons/all-files/io5/IoDownloadOutline';
|
||||
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import GSheetInfo from './GSheetInfo';
|
||||
import GSheetSetup from './GSheetSetup';
|
||||
import ImportMap from './ImportMap';
|
||||
import ImportReview from './ImportReview';
|
||||
import { useSheetStore } from './useSheetStore';
|
||||
|
||||
import style from './SourcesPanel.module.scss';
|
||||
|
||||
export default function SourcesPanel() {
|
||||
const [importFlow, setImportFlow] = useState<'none' | 'excel' | 'gsheet'>('none');
|
||||
|
||||
const hasDataSource = useSheetStore((state) => state.stepData.worksheet.available);
|
||||
const rundown = useSheetStore((state) => state.rundown);
|
||||
const userFields = useSheetStore((state) => state.userFields);
|
||||
const hasData = rundown && userFields;
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFile = () => console.error('not yet implementeed');
|
||||
|
||||
const handleUpload = () => {
|
||||
fileInputRef.current?.click();
|
||||
setImportFlow('excel');
|
||||
};
|
||||
|
||||
const openGSheetFlow = () => {
|
||||
setImportFlow('gsheet');
|
||||
};
|
||||
|
||||
const cancelGSheetFlow = () => {
|
||||
setImportFlow('none');
|
||||
};
|
||||
|
||||
const isExcelFlow = importFlow === 'excel';
|
||||
const isGSheetFlow = importFlow === 'gsheet';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel.Header>Data sources</Panel.Header>
|
||||
<Panel.Section>
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>Synchronise your rundown with an external source</Panel.SubHeader>
|
||||
<GSheetInfo />
|
||||
{!isGSheetFlow && (
|
||||
<>
|
||||
<Input ref={fileInputRef} style={{ display: 'none' }} type='file' onChange={handleFile} accept='.xlsx' />
|
||||
<div className={style.uploadSection}>
|
||||
<div>
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
leftIcon={<IoDownloadOutline />}
|
||||
onClick={handleUpload}
|
||||
isDisabled
|
||||
>
|
||||
Import from spreadsheet
|
||||
</Button>
|
||||
<Panel.Description>Accepts .xlsx files</Panel.Description>
|
||||
</div>
|
||||
<div>
|
||||
<Button variant='ontime-filled' size='sm' leftIcon={<IoCloudOutline />} onClick={openGSheetFlow}>
|
||||
Synchronise with Google
|
||||
</Button>
|
||||
<Panel.Description>Start authentication process</Panel.Description>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isGSheetFlow && <GSheetSetup cancel={cancelGSheetFlow} />}
|
||||
{isExcelFlow && <Panel.Title>Not yet implemented</Panel.Title>}
|
||||
{hasDataSource && <ImportMap />}
|
||||
{hasData && <ImportReview rundown={rundown} userFields={userFields} />}
|
||||
</Panel.Card>
|
||||
</Panel.Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { ChangeEvent } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { OntimeRundown, UserFields } from 'ontime-types';
|
||||
import { ExcelImportMap } from 'ontime-utils';
|
||||
|
||||
import { RUNDOWN, USERFIELDS } from '../../../../common/api/apiConstants';
|
||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
||||
import {
|
||||
getAuthentication,
|
||||
getClientSecret,
|
||||
getSheetsAuthUrl,
|
||||
patchData,
|
||||
postId,
|
||||
postPreviewSheet,
|
||||
postPushSheet,
|
||||
postWorksheet,
|
||||
uploadSheetClientFile,
|
||||
} from '../../../../common/api/ontimeApi';
|
||||
import { openLink } from '../../../../common/utils/linkUtils';
|
||||
|
||||
import { useSheetStore } from './useSheetStore';
|
||||
|
||||
// TODO: recover useEffect for resuming previous state
|
||||
export default function useGoogleSheet() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// functions push data to store
|
||||
const setClientSecret = useSheetStore((state) => state.setClientSecret);
|
||||
const patchStepData = useSheetStore((state) => state.patchStepData);
|
||||
const setSheetId = useSheetStore((state) => state.setSheetId);
|
||||
const setWorksheetOptions = useSheetStore((state) => state.setWorksheetOptions);
|
||||
const setRundown = useSheetStore((state) => state.setRundown);
|
||||
const setUserFields = useSheetStore((state) => state.setUserFields);
|
||||
|
||||
/** receives a client secrets file and passes on to the server */
|
||||
const handleClientSecret = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!event.target.files?.length) {
|
||||
patchStepData({
|
||||
clientSecret: { available: true, error: 'Missing file' },
|
||||
authenticate: { available: false, error: '' },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const selectedFile = event.target.files[0];
|
||||
await uploadSheetClientFile(selectedFile);
|
||||
// TODO: why do we need this call?
|
||||
await getClientSecret();
|
||||
setClientSecret(selectedFile);
|
||||
patchStepData({
|
||||
clientSecret: { available: true, error: '' },
|
||||
authenticate: { available: true, error: '' },
|
||||
});
|
||||
} catch (error) {
|
||||
patchStepData({
|
||||
clientSecret: { available: true, error: maybeAxiosError(error) },
|
||||
authenticate: { available: false, error: '' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** authenticate with the Google Sheets API */
|
||||
const handleAuthenticate = async () => {
|
||||
try {
|
||||
const authLink = await getSheetsAuthUrl();
|
||||
|
||||
// request window to open link and check auth when user is back
|
||||
openLink(authLink);
|
||||
window.addEventListener('focus', async () => await getAuthentication(), { once: true });
|
||||
|
||||
patchStepData({
|
||||
authenticate: { available: true, error: '' },
|
||||
sheetId: { available: true, error: '' },
|
||||
});
|
||||
} catch (error) {
|
||||
patchStepData({
|
||||
authenticate: { available: true, error: maybeAxiosError(error) },
|
||||
sheetId: { available: false, error: '' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/** fetches data from a Google Sheet by its ID */
|
||||
const handleConnect = async (sheetId: string) => {
|
||||
try {
|
||||
setSheetId(sheetId);
|
||||
const data = await postId(sheetId);
|
||||
setWorksheetOptions(data.worksheetOptions);
|
||||
patchStepData({ worksheet: { available: true, error: '' } });
|
||||
} catch (error) {
|
||||
patchStepData({
|
||||
sheetId: { available: true, error: maybeAxiosError(error) },
|
||||
worksheet: { available: false, error: '' },
|
||||
pullPush: { available: false, error: '' },
|
||||
});
|
||||
setWorksheetOptions([]);
|
||||
}
|
||||
};
|
||||
|
||||
/** fetches data from a worksheet by its ID */
|
||||
const handleImportPreview = async (sheetId: string, worksheet: string, fileOptions: ExcelImportMap) => {
|
||||
try {
|
||||
// update worksheet data in the server
|
||||
await postWorksheet(sheetId, worksheet);
|
||||
|
||||
// get data from google
|
||||
const data = await postPreviewSheet(sheetId, fileOptions);
|
||||
setRundown(data.rundown);
|
||||
setUserFields(data.userFields);
|
||||
} catch (error) {
|
||||
patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } });
|
||||
}
|
||||
};
|
||||
|
||||
/** writes data to a worksheet by its ID */
|
||||
const handleExport = async (sheetId: string, worksheet: string, fileOptions: ExcelImportMap) => {
|
||||
try {
|
||||
// update worksheet data in the server
|
||||
await postWorksheet(sheetId, worksheet);
|
||||
|
||||
// write data to google
|
||||
await postPushSheet(sheetId, fileOptions);
|
||||
patchStepData({ pullPush: { available: false, error: '' } });
|
||||
} catch (error) {
|
||||
patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } });
|
||||
}
|
||||
};
|
||||
|
||||
/** applies rundown and userfields to current project */
|
||||
const handleImport = async (rundown: OntimeRundown, userFields: UserFields) => {
|
||||
try {
|
||||
await patchData({ rundown, userFields });
|
||||
queryClient.setQueryData(RUNDOWN, rundown);
|
||||
queryClient.setQueryData(USERFIELDS, userFields);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [...RUNDOWN, ...USERFIELDS],
|
||||
});
|
||||
} catch (error) {
|
||||
patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleClientSecret,
|
||||
handleAuthenticate,
|
||||
handleConnect,
|
||||
handleImportPreview,
|
||||
handleImport,
|
||||
handleExport,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { OntimeRundown, UserFields } from 'ontime-types';
|
||||
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
|
||||
import { create } from 'zustand';
|
||||
|
||||
// TODO: persist excelFileOptions to localStorage
|
||||
|
||||
type SheetStore = {
|
||||
clientSecret: File | null;
|
||||
rundown: OntimeRundown | null;
|
||||
userFields: UserFields | null;
|
||||
sheetId: string | null;
|
||||
worksheet: string | null;
|
||||
worksheetOptions: string[] | null;
|
||||
excelFileOptions: ExcelImportMap;
|
||||
stepData: typeof initialStepData;
|
||||
setClientSecret: (clientSecret: File | null) => void;
|
||||
setRundown: (rundown: OntimeRundown | null) => void;
|
||||
setUserFields: (userFields: UserFields | null) => void;
|
||||
setSheetId: (sheetId: string) => void;
|
||||
setWorksheet: (worksheet: string) => void;
|
||||
setWorksheetOptions: (worksheetOptions: string[] | null) => void;
|
||||
patchExcelFileOptions: <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => void;
|
||||
patchStepData: (patch: Partial<typeof initialStepData>) => void;
|
||||
reset: () => void;
|
||||
resetPreview: () => void;
|
||||
};
|
||||
|
||||
const initialStepData = {
|
||||
clientSecret: { available: true, error: '' },
|
||||
authenticate: { available: false, error: '' },
|
||||
sheetId: { available: false, error: '' },
|
||||
worksheet: { available: false, error: '' },
|
||||
pullPush: { available: false, error: '' },
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
clientSecret: null,
|
||||
rundown: null,
|
||||
userFields: null,
|
||||
sheetId: null,
|
||||
worksheet: null,
|
||||
worksheetOptions: null,
|
||||
excelFileOptions: defaultExcelImportMap,
|
||||
stepData: initialStepData,
|
||||
};
|
||||
|
||||
export const useSheetStore = create<SheetStore>((set, get) => ({
|
||||
...initialState,
|
||||
setClientSecret: (clientSecret: File | null) => set({ clientSecret }),
|
||||
setRundown: (rundown: OntimeRundown | null) => set({ rundown }),
|
||||
setUserFields: (userFields: UserFields | null) => set({ userFields }),
|
||||
setSheetId: (sheetId: string) => set({ sheetId }),
|
||||
setWorksheet: (worksheet: string) => set({ worksheet }),
|
||||
setWorksheetOptions: (worksheetOptions: string[] | null) => set({ worksheetOptions }),
|
||||
patchExcelFileOptions: <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => {
|
||||
const excelFileOptions = get().excelFileOptions;
|
||||
if (excelFileOptions[field] !== value) {
|
||||
excelFileOptions[field] = value;
|
||||
}
|
||||
},
|
||||
patchStepData: (patch: Partial<typeof initialStepData>) => {
|
||||
const stepData = get().stepData;
|
||||
set({ stepData: { ...stepData, ...patch } });
|
||||
},
|
||||
reset: () => set(initialState),
|
||||
resetPreview: () => set({ rundown: null, userFields: null }),
|
||||
}));
|
||||
@@ -16,14 +16,17 @@ export const settingPanels: Readonly<SettingsOption[]> = [
|
||||
{
|
||||
id: 'project_settings',
|
||||
label: 'Project Settings',
|
||||
secondary: [{ id: 'project_settings__custom', label: 'Custom Fields' }],
|
||||
secondary: [{ id: 'project_settings__custom', label: 'Custom fields' }],
|
||||
},
|
||||
{ id: 'interface', label: 'Interface' },
|
||||
{ id: 'views', label: 'Views' },
|
||||
{
|
||||
id: 'sources',
|
||||
label: 'Data Sources',
|
||||
secondary: [{ id: 'sources__gsheet', label: 'Sync with Google Sheet' }],
|
||||
secondary: [
|
||||
{ id: 'sources__xlsx', label: 'Import spreadsheet' },
|
||||
{ id: 'sources__gsheet', label: 'Sync with Google Sheet' },
|
||||
],
|
||||
split: true,
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user