mirror of
https://github.com/cpvalente/ontime.git
synced 2026-09-09 00:09:15 +00:00
Sheets settings (#774)
* wip: migrate sheet settings --------- Co-authored-by: arc-alex <ac@omnivox.dk>
This commit is contained in:
@@ -275,7 +275,8 @@ export const uploadSheetClientFile = async (file: File) => {
|
|||||||
/**
|
/**
|
||||||
* @description STEP 1 test
|
* @description STEP 1 test
|
||||||
*/
|
*/
|
||||||
export const getClientSecrect = async () => {
|
// TODO: do we still need this?
|
||||||
|
export const getClientSecret = async () => {
|
||||||
const response = await axios.get(`${ontimeURL}/sheet/clientsecret`);
|
const response = await axios.get(`${ontimeURL}/sheet/clientsecret`);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
@@ -300,32 +301,32 @@ export const getAuthentication = async () => {
|
|||||||
* @description STEP 3
|
* @description STEP 3
|
||||||
* @returns worksheetOptions
|
* @returns worksheetOptions
|
||||||
*/
|
*/
|
||||||
export const postId = async (id: string) => {
|
export const postId = async (sheetId: string) => {
|
||||||
const response = await axios.post(`${ontimeURL}/sheet/id`, { id });
|
const response = await axios.post(`${ontimeURL}/sheet/sheetId`, { sheetId });
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description STEP 4
|
* @description STEP 4
|
||||||
*/
|
*/
|
||||||
export const postWorksheet = async (id: string, worksheet: string) => {
|
export const postWorksheet = async (sheetId: string, worksheet: string) => {
|
||||||
const response = await axios.post(`${ontimeURL}/sheet/worksheet`, { id, worksheet });
|
const response = await axios.post(`${ontimeURL}/sheet/worksheet`, { sheetId, worksheet });
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description STEP 5
|
* @description STEP 5
|
||||||
*/
|
*/
|
||||||
export const postPreviewSheet = async (id: string, options: ExcelImportMap) => {
|
export const postPreviewSheet = async (sheetId: string, options: ExcelImportMap) => {
|
||||||
const response = await axios.post(`${ontimeURL}/sheet/pull`, { id, options });
|
const response = await axios.post(`${ontimeURL}/sheet-pull`, { sheetId, options });
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description STEP 5
|
* @description STEP 5
|
||||||
*/
|
*/
|
||||||
export const postPushSheet = async (id: string, options: ExcelImportMap) => {
|
export const postPushSheet = async (sheetId: string, options: ExcelImportMap) => {
|
||||||
const response = await axios.post(`${ontimeURL}/sheet-push`, { id, options });
|
const response = await axios.post(`${ontimeURL}/sheet-push`, { sheetId, options });
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const ContextMenu = ({ children }: ContextMenuProps) => {
|
|||||||
<>
|
<>
|
||||||
{children}
|
{children}
|
||||||
<div className={style.contextMenuBackdrop} />
|
<div className={style.contextMenuBackdrop} />
|
||||||
<Menu isOpen gutter={0} onClose={onClose} isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
|
<Menu isOpen size='sm' gutter={0} onClose={onClose} isLazy lazyBehavior='unmount' variant='ontime-on-dark'>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
className={style.contextMenuButton}
|
className={style.contextMenuButton}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
color: $blue-500;
|
color: $blue-500;
|
||||||
transition-property: color;
|
transition-property: color;
|
||||||
transition-duration: $transition-time-action;
|
transition-duration: $transition-time-action;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
&.inline {
|
&.inline {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { InputGroup, Tooltip } from '@chakra-ui/react';
|
import { InputGroup } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
|
||||||
import { cx } from '../../../utils/styleUtils';
|
import { cx } from '../../../utils/styleUtils';
|
||||||
|
|
||||||
import TimeInput from './TimeInput';
|
import TimeInput from './TimeInput';
|
||||||
@@ -24,7 +23,6 @@ export default function TimeInputWithButton<T extends string>(props: PropsWithCh
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<InputGroup size='sm' className={inputClasses} width='fit-content'>
|
<InputGroup size='sm' className={inputClasses} width='fit-content'>
|
||||||
<Tooltip label={placeholder} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
|
||||||
<TimeInput<T>
|
<TimeInput<T>
|
||||||
name={name}
|
name={name}
|
||||||
submitHandler={submitHandler}
|
submitHandler={submitHandler}
|
||||||
@@ -33,7 +31,6 @@ export default function TimeInputWithButton<T extends string>(props: PropsWithCh
|
|||||||
className={style.inputField}
|
className={style.inputField}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
{children}
|
{children}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import IntegrationsPanel from './panel/integrations-panel/IntegrationsPanel';
|
|||||||
import LogPanel from './panel/log-panel/LogPanel';
|
import LogPanel from './panel/log-panel/LogPanel';
|
||||||
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
import ProjectPanel from './panel/project-panel/ProjectPanel';
|
||||||
import ProjectSettingsPanel from './panel/project-settings-panel/ProjectSettingsPanel';
|
import ProjectSettingsPanel from './panel/project-settings-panel/ProjectSettingsPanel';
|
||||||
|
import SourcesPanel from './panel/sources-panel/SourcesPanel';
|
||||||
import PanelContent from './panel-content/PanelContent';
|
import PanelContent from './panel-content/PanelContent';
|
||||||
import PanelList from './panel-list/PanelList';
|
import PanelList from './panel-list/PanelList';
|
||||||
import { useSettingsStore } from './settingsStore';
|
import { useSettingsStore } from './settingsStore';
|
||||||
@@ -28,6 +29,7 @@ export default function AppSettings() {
|
|||||||
<PanelList />
|
<PanelList />
|
||||||
<PanelContent onClose={closeSettings}>
|
<PanelContent onClose={closeSettings}>
|
||||||
{selectedPanel === 'project' && <ProjectPanel />}
|
{selectedPanel === 'project' && <ProjectPanel />}
|
||||||
|
{selectedPanel === 'sources' && <SourcesPanel />}
|
||||||
{selectedPanel === 'integrations' && <IntegrationsPanel />}
|
{selectedPanel === 'integrations' && <IntegrationsPanel />}
|
||||||
{selectedPanel === 'project_settings' && <ProjectSettingsPanel />}
|
{selectedPanel === 'project_settings' && <ProjectSettingsPanel />}
|
||||||
{selectedPanel === 'about' && <AboutPanel />}
|
{selectedPanel === 'about' && <AboutPanel />}
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ $inner-padding: 1rem;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.375rem;
|
||||||
padding-left: $inner-padding;
|
padding: 0 2rem;
|
||||||
|
font-weight: 600;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
color: $gray-300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
@@ -49,7 +51,7 @@ $inner-padding: 1rem;
|
|||||||
|
|
||||||
.pad {
|
.pad {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
max-height: 500px;
|
max-height: 550px;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +82,7 @@ $inner-padding: 1rem;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.listGroup {
|
.listGroup {
|
||||||
padding: 0.5rem 1rem 0 1rem;
|
padding: 1rem 2rem 0 2rem;
|
||||||
|
|
||||||
> li:not(:last-child) {
|
> li:not(:last-child) {
|
||||||
border-bottom: 1px solid $white-10;
|
border-bottom: 1px solid $white-10;
|
||||||
|
|||||||
@@ -17,18 +17,14 @@ export default function AboutPanel() {
|
|||||||
</Panel.Paragraph>
|
</Panel.Paragraph>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Card>
|
|
||||||
<Panel.SubHeader>Links</Panel.SubHeader>
|
<Panel.SubHeader>Links</Panel.SubHeader>
|
||||||
<ExternalLink href={gitbookUrl}>Read the docs over at GitBook</ExternalLink>
|
<ExternalLink href={gitbookUrl}>Read the docs over at GitBook</ExternalLink>
|
||||||
<ExternalLink href={githubUrl}>Follow the project on GitHub</ExternalLink>
|
<ExternalLink href={githubUrl}>Follow the project on GitHub</ExternalLink>
|
||||||
</Panel.Card>
|
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Card>
|
|
||||||
<Panel.SubHeader>Current version</Panel.SubHeader>
|
<Panel.SubHeader>Current version</Panel.SubHeader>
|
||||||
<Panel.Paragraph>{`You are currently using Ontime ${version}`}</Panel.Paragraph>
|
<Panel.Paragraph>{`You are currently using Ontime ${version}`}</Panel.Paragraph>
|
||||||
<CheckUpdatesButton version={version} />
|
<CheckUpdatesButton version={version} />
|
||||||
</Panel.Card>
|
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export default function CheckUpdatesButton(props: CheckUpdatesButtonProps) {
|
|||||||
|
|
||||||
return (
|
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
|
Check for updates
|
||||||
</Button>
|
</Button>
|
||||||
<ResolveUpdateMessage updateMessage={updateMessage} />
|
<ResolveUpdateMessage updateMessage={updateMessage} />
|
||||||
|
|||||||
+2
-2
@@ -2,8 +2,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fitContents {
|
.fitContents.fitContents {
|
||||||
width: max-content !important; /* override chakra */
|
width: max-content; /* override chakra */
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ export default function IntegrationsPanel() {
|
|||||||
<Alert status='info' variant='ontime-on-dark-info'>
|
<Alert status='info' variant='ontime-on-dark-info'>
|
||||||
<AlertIcon />
|
<AlertIcon />
|
||||||
<AlertDescription>
|
<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 />
|
<br />
|
||||||
Currently supported protocols are OSC (Open Sound Control), HTTP and Websockets. <br />
|
Currently supported protocols are OSC (Open Sound Control), HTTP and Websockets. <br />
|
||||||
WebSockets are used for Ontime and cannot be configured independently. <br />
|
WebSockets are used for Ontime and cannot be configured independently. <br />
|
||||||
|
|||||||
@@ -97,7 +97,11 @@ export default function OscIntegrations() {
|
|||||||
</div>
|
</div>
|
||||||
</Panel.SubHeader>
|
</Panel.SubHeader>
|
||||||
|
|
||||||
|
<Panel.Divider />
|
||||||
|
|
||||||
|
|
||||||
<Panel.Section as='form' id='osc-form' onSubmit={handleSubmit(onSubmit)} onKeyDown={preventEscape}>
|
<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>}
|
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
|
|||||||
@@ -19,9 +19,14 @@ export default function ProjectPanel() {
|
|||||||
<Panel.Card>
|
<Panel.Card>
|
||||||
<Panel.SubHeader>
|
<Panel.SubHeader>
|
||||||
Manage projects
|
Manage projects
|
||||||
<Button variant='ontime-filled' onClick={handleToggleCreate}>
|
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||||
New
|
<Button variant='ontime-subtle' onClick={handleToggleCreate} size='sm'>
|
||||||
|
Import
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant='ontime-subtle' onClick={handleToggleCreate} size='sm'>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Panel.SubHeader>
|
</Panel.SubHeader>
|
||||||
<ProjectList onToggleCreate={handleToggleCreate} isCreatingProject={isCreatingProject} />
|
<ProjectList onToggleCreate={handleToggleCreate} isCreatingProject={isCreatingProject} />
|
||||||
</Panel.Card>
|
</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',
|
id: 'project_settings',
|
||||||
label: '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: 'interface', label: 'Interface' },
|
||||||
{ id: 'views', label: 'Views' },
|
{ id: 'views', label: 'Views' },
|
||||||
{
|
{
|
||||||
id: 'sources',
|
id: 'sources',
|
||||||
label: 'Data 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,
|
split: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'
|
|||||||
import AppSettings from '../app-settings/AppSettings';
|
import AppSettings from '../app-settings/AppSettings';
|
||||||
import { SettingsOptionId, useSettingsStore } from '../app-settings/settingsStore';
|
import { SettingsOptionId, useSettingsStore } from '../app-settings/settingsStore';
|
||||||
import MenuBar from '../menu/MenuBar';
|
import MenuBar from '../menu/MenuBar';
|
||||||
import SheetsModal from '../modals/sheets-modal/SheetsModal';
|
|
||||||
import UploadModal from '../modals/upload-modal/UploadModal';
|
import UploadModal from '../modals/upload-modal/UploadModal';
|
||||||
import Overview from '../overview/Overview';
|
import Overview from '../overview/Overview';
|
||||||
|
|
||||||
@@ -26,7 +25,6 @@ export default function Editor() {
|
|||||||
|
|
||||||
const { isOpen: isOldSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
|
const { isOpen: isOldSettingsOpen, onOpen: onSettingsOpen, onClose: onSettingsClose } = useDisclosure();
|
||||||
const { isOpen: isUploadModalOpen, onOpen: onUploadModalOpen, onClose: onUploadModalClose } = useDisclosure();
|
const { isOpen: isUploadModalOpen, onOpen: onUploadModalOpen, onClose: onUploadModalClose } = useDisclosure();
|
||||||
const { isOpen: isSheetsOpen, onOpen: onSheetsOpen, onClose: onSheetsClose } = useDisclosure();
|
|
||||||
|
|
||||||
// Set window title
|
// Set window title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -40,7 +38,6 @@ export default function Editor() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
|
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
|
||||||
<SettingsModal isOpen={isOldSettingsOpen} onClose={onSettingsClose} />
|
<SettingsModal isOpen={isOldSettingsOpen} onClose={onSettingsClose} />
|
||||||
<SheetsModal onClose={onSheetsClose} isOpen={isSheetsOpen} />
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
@@ -52,8 +49,6 @@ export default function Editor() {
|
|||||||
onUploadOpen={onUploadModalOpen}
|
onUploadOpen={onUploadModalOpen}
|
||||||
openSettings={handleSettings}
|
openSettings={handleSettings}
|
||||||
isSettingsOpen={isSettingsOpen}
|
isSettingsOpen={isSettingsOpen}
|
||||||
isSheetsOpen={isSheetsOpen}
|
|
||||||
onSheetsOpen={onSheetsOpen}
|
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
{showSettings ? (
|
{showSettings ? (
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { memo, useCallback, useEffect } from 'react';
|
import { memo, useCallback, useEffect } from 'react';
|
||||||
import { IconButton, MenuButton, Tooltip } from '@chakra-ui/react';
|
import { IconButton, MenuButton, Tooltip } from '@chakra-ui/react';
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
import { IoCloud } from '@react-icons/all-files/io5/IoCloud';
|
|
||||||
import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
|
|
||||||
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
||||||
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
import { IoPlay } from '@react-icons/all-files/io5/IoPlay';
|
||||||
import { IoPushOutline } from '@react-icons/all-files/io5/IoPushOutline';
|
import { IoPushOutline } from '@react-icons/all-files/io5/IoPushOutline';
|
||||||
@@ -25,8 +23,6 @@ interface MenuBarProps {
|
|||||||
onSettingsClose: () => void;
|
onSettingsClose: () => void;
|
||||||
isUploadOpen: boolean;
|
isUploadOpen: boolean;
|
||||||
onUploadOpen: () => void;
|
onUploadOpen: () => void;
|
||||||
isSheetsOpen: boolean;
|
|
||||||
onSheetsOpen: () => void;
|
|
||||||
openSettings: (newTab?: string) => void;
|
openSettings: (newTab?: string) => void;
|
||||||
isSettingsOpen: boolean;
|
isSettingsOpen: boolean;
|
||||||
}
|
}
|
||||||
@@ -52,8 +48,6 @@ const MenuBar = (props: MenuBarProps) => {
|
|||||||
onUploadOpen,
|
onUploadOpen,
|
||||||
openSettings,
|
openSettings,
|
||||||
isSettingsOpen,
|
isSettingsOpen,
|
||||||
isSheetsOpen,
|
|
||||||
onSheetsOpen,
|
|
||||||
} = props;
|
} = props;
|
||||||
const { isElectron, sendToElectron } = useElectronEvent();
|
const { isElectron, sendToElectron } = useElectronEvent();
|
||||||
|
|
||||||
@@ -141,15 +135,6 @@ const MenuBar = (props: MenuBarProps) => {
|
|||||||
aria-label='Edit mode'
|
aria-label='Edit mode'
|
||||||
/>
|
/>
|
||||||
<div className={style.gap} />
|
<div className={style.gap} />
|
||||||
<TooltipActionBtn
|
|
||||||
{...buttonStyle}
|
|
||||||
isDisabled={appMode === AppMode.Run}
|
|
||||||
icon={isSheetsOpen ? <IoCloud /> : <IoCloudOutline />}
|
|
||||||
className={isSheetsOpen ? style.open : ''}
|
|
||||||
clickHandler={onSheetsOpen}
|
|
||||||
tooltip='Sheets'
|
|
||||||
aria-label='Sheets'
|
|
||||||
/>
|
|
||||||
<TooltipActionBtn
|
<TooltipActionBtn
|
||||||
{...buttonStyle}
|
{...buttonStyle}
|
||||||
isDisabled={appMode === AppMode.Run}
|
isDisabled={appMode === AppMode.Run}
|
||||||
|
|||||||
@@ -1,460 +0,0 @@
|
|||||||
import { ChangeEvent, useEffect, useRef, useState } from 'react';
|
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
AlertDescription,
|
|
||||||
AlertIcon,
|
|
||||||
AlertTitle,
|
|
||||||
Button,
|
|
||||||
HStack,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
Select,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { OntimeRundown, UserFields } from 'ontime-types';
|
|
||||||
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { RUNDOWN, USERFIELDS } from '../../../common/api/apiConstants';
|
|
||||||
import { maybeAxiosError } from '../../../common/api/apiUtils';
|
|
||||||
import {
|
|
||||||
getAuthentication,
|
|
||||||
getClientSecrect,
|
|
||||||
getSheetsAuthUrl,
|
|
||||||
patchData,
|
|
||||||
postId,
|
|
||||||
postPreviewSheet,
|
|
||||||
postPushSheet,
|
|
||||||
postWorksheet,
|
|
||||||
uploadSheetClientFile,
|
|
||||||
} from '../../../common/api/ontimeApi';
|
|
||||||
import { userFieldsPlaceholder } from '../../../common/models/UserFields';
|
|
||||||
import { openLink } from '../../../common/utils/linkUtils';
|
|
||||||
import ModalLink from '../ModalLink';
|
|
||||||
import PreviewExcel from '../upload-modal/preview/PreviewExcel';
|
|
||||||
import ExcelFileOptions from '../upload-modal/upload-options/ExcelFileOptions';
|
|
||||||
|
|
||||||
import Step from './Step';
|
|
||||||
|
|
||||||
interface SheetsModalProps {
|
|
||||||
onClose: () => void;
|
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SheetsModal(props: SheetsModalProps) {
|
|
||||||
const { isOpen, onClose } = props;
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const [rundown, setRundown] = useState<OntimeRundown | null>(null);
|
|
||||||
const [userFields, setUserFields] = useState<UserFields | null>(null);
|
|
||||||
|
|
||||||
const [id, setSheetId] = useState('');
|
|
||||||
const [worksheet, setWorksheet] = useState('');
|
|
||||||
const [worksheetOptions, setWorksheetOptions] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const [direction, setDirection] = useState('none');
|
|
||||||
const excelFileOptions = useRef<ExcelImportMap>(defaultExcelImportMap);
|
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const [state, setState] = useState({
|
|
||||||
clientSecret: { complete: false, message: '' },
|
|
||||||
authenticate: { complete: false, message: '' },
|
|
||||||
id: { complete: false, message: '' },
|
|
||||||
worksheet: { complete: false, message: '' },
|
|
||||||
pullPush: { complete: false, message: '' },
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) {
|
|
||||||
setDirection('none');
|
|
||||||
testClientSecret();
|
|
||||||
if (state.clientSecret.complete) testAuthentication();
|
|
||||||
if (state.authenticate.complete) testSheetId();
|
|
||||||
}
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setRundown(null);
|
|
||||||
setUserFields(null);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//STEP-1 Upload Client ID
|
|
||||||
const handleClick = () => {
|
|
||||||
fileInputRef.current?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFile = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (!event.target.files?.length) {
|
|
||||||
setState({
|
|
||||||
clientSecret: { complete: false, message: 'Missing file' },
|
|
||||||
authenticate: { complete: false, message: '' },
|
|
||||||
id: { complete: false, message: '' },
|
|
||||||
worksheet: { complete: false, message: '' },
|
|
||||||
pullPush: { complete: false, message: '' },
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selectedFile = event.target.files[0];
|
|
||||||
uploadSheetClientFile(selectedFile)
|
|
||||||
.then(() => {
|
|
||||||
setState({ ...state, clientSecret: { complete: true, message: '' } });
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
const message = maybeAxiosError(err);
|
|
||||||
setState({
|
|
||||||
clientSecret: { complete: false, message },
|
|
||||||
authenticate: { complete: false, message: '' },
|
|
||||||
id: { complete: false, message: '' },
|
|
||||||
worksheet: { complete: false, message: '' },
|
|
||||||
pullPush: { complete: false, message: '' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const testClientSecret = () => {
|
|
||||||
getClientSecrect()
|
|
||||||
.then(() => {
|
|
||||||
setState({ ...state, clientSecret: { complete: true, message: '' } });
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
const message = maybeAxiosError(err);
|
|
||||||
setState({
|
|
||||||
clientSecret: { complete: false, message },
|
|
||||||
authenticate: { complete: false, message: '' },
|
|
||||||
id: { complete: false, message: '' },
|
|
||||||
worksheet: { complete: false, message: '' },
|
|
||||||
pullPush: { complete: false, message: '' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//STEP-2 Authenticate
|
|
||||||
const handleAuthenticate = () => {
|
|
||||||
getSheetsAuthUrl()
|
|
||||||
.then((data) => {
|
|
||||||
openLink(data);
|
|
||||||
window.addEventListener('focus', () => testAuthentication(), { once: true });
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
const message = maybeAxiosError(err);
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
authenticate: { complete: false, message },
|
|
||||||
id: { complete: false, message: '' },
|
|
||||||
worksheet: { complete: false, message: '' },
|
|
||||||
pullPush: { complete: false, message: '' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const testAuthentication = () => {
|
|
||||||
getAuthentication()
|
|
||||||
.then(() => {
|
|
||||||
setState({ ...state, authenticate: { complete: true, message: '' } });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
const message = maybeAxiosError(error);
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
authenticate: { complete: false, message },
|
|
||||||
id: { complete: false, message: '' },
|
|
||||||
worksheet: { complete: false, message: '' },
|
|
||||||
pullPush: { complete: false, message: '' },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//STEP-3 set sheet ID
|
|
||||||
const testSheetId = () => {
|
|
||||||
postId(id)
|
|
||||||
.then((data) => {
|
|
||||||
setState({ ...state, id: { complete: true, message: '' } });
|
|
||||||
setWorksheetOptions(data.worksheetOptions);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
const message = maybeAxiosError(err);
|
|
||||||
setState({
|
|
||||||
...state,
|
|
||||||
id: { complete: false, message },
|
|
||||||
worksheet: { complete: false, message: '' },
|
|
||||||
pullPush: { complete: false, message: '' },
|
|
||||||
});
|
|
||||||
setWorksheetOptions([]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//STEP-4 Select Worksheet
|
|
||||||
const testWorksheet = (value: string) => {
|
|
||||||
excelFileOptions.current.worksheet = value;
|
|
||||||
setWorksheet(value);
|
|
||||||
postWorksheet(id, worksheet)
|
|
||||||
.then(() => {
|
|
||||||
setState({ ...state, worksheet: { complete: true, message: '' } });
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
const message = maybeAxiosError(err);
|
|
||||||
setState({ ...state, worksheet: { complete: false, message }, pullPush: { complete: false, message: '' } });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//STEP-5 Upload / Download
|
|
||||||
const updateExcelFileOptions = <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => {
|
|
||||||
if (excelFileOptions.current[field] !== value) {
|
|
||||||
excelFileOptions.current = { ...excelFileOptions.current, [field]: value };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePullData = () => {
|
|
||||||
postPreviewSheet(id, excelFileOptions.current)
|
|
||||||
.then((data) => {
|
|
||||||
setRundown(data.rundown);
|
|
||||||
setUserFields(data.userFields);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
const message = maybeAxiosError(error);
|
|
||||||
setDirection('none');
|
|
||||||
setState({ ...state, pullPush: { complete: false, message } });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePushData = () => {
|
|
||||||
postPushSheet(id, excelFileOptions.current)
|
|
||||||
.then(() => {
|
|
||||||
setDirection('none');
|
|
||||||
setState({ ...state, pullPush: { complete: true, message: '' } });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
const message = maybeAxiosError(error);
|
|
||||||
setDirection('none');
|
|
||||||
setState({ ...state, pullPush: { complete: false, message } });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//GET preview
|
|
||||||
const handleFinalise = async () => {
|
|
||||||
if (rundown && userFields) {
|
|
||||||
let doClose = false;
|
|
||||||
try {
|
|
||||||
await patchData({ rundown, userFields });
|
|
||||||
queryClient.setQueryData(RUNDOWN, rundown);
|
|
||||||
queryClient.setQueryData(USERFIELDS, userFields);
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [...RUNDOWN, ...USERFIELDS],
|
|
||||||
});
|
|
||||||
doClose = true;
|
|
||||||
} catch (error) {
|
|
||||||
const message = maybeAxiosError(error);
|
|
||||||
console.error(message);
|
|
||||||
} finally {
|
|
||||||
if (doClose) {
|
|
||||||
handleClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
onClose={handleClose}
|
|
||||||
isOpen={isOpen}
|
|
||||||
closeOnOverlayClick={false}
|
|
||||||
motionPreset='slideInBottom'
|
|
||||||
size='xl'
|
|
||||||
scrollBehavior='inside'
|
|
||||||
preserveScrollBarGap
|
|
||||||
variant='ontime-upload'
|
|
||||||
>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>Rundown from sheets (experimental)</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody>
|
|
||||||
<Alert status='info' variant='ontime-on-light-info'>
|
|
||||||
<AlertIcon />
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
||||||
<AlertTitle>Sync with Google Sheets</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
<ModalLink href='https://ontime.gitbook.io/v2/features/google-sheet'>
|
|
||||||
For more information, see the docs
|
|
||||||
</ModalLink>
|
|
||||||
</AlertDescription>
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
{!rundown ? (
|
|
||||||
direction === 'up' || direction === 'down' ? (
|
|
||||||
<ExcelFileOptions optionsRef={excelFileOptions} updateOptions={updateExcelFileOptions} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Step
|
|
||||||
title='1 - Upload OAuth 2.0 Client ID'
|
|
||||||
completed={state.clientSecret.complete}
|
|
||||||
disabled={false}
|
|
||||||
error={state.clientSecret.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
ref={fileInputRef}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
type='file'
|
|
||||||
onChange={handleFile}
|
|
||||||
accept='.json'
|
|
||||||
data-testid='file-input'
|
|
||||||
/>
|
|
||||||
<div style={{ display: 'flex', gap: '1em' }}>
|
|
||||||
<Button size='sm' variant='ontime-subtle-on-light' onClick={handleClick}>
|
|
||||||
{state.clientSecret.complete ? 'Reupload Client ID' : 'Upload Client ID'}
|
|
||||||
</Button>
|
|
||||||
<Button size='sm' variant='ontime-ghosted-on-light' onClick={testClientSecret}>
|
|
||||||
Retry Client ID
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step
|
|
||||||
title='2 - Authenticate with Google'
|
|
||||||
completed={state.authenticate.complete}
|
|
||||||
disabled={!state.clientSecret.complete}
|
|
||||||
error={state.authenticate.message}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', gap: '1em' }}>
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-subtle-on-light'
|
|
||||||
onClick={handleAuthenticate}
|
|
||||||
isDisabled={!state.clientSecret.complete}
|
|
||||||
>
|
|
||||||
Authenticate
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-ghosted-on-light'
|
|
||||||
onClick={testAuthentication}
|
|
||||||
isDisabled={!state.clientSecret.complete}
|
|
||||||
>
|
|
||||||
Retry Connection
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step
|
|
||||||
title='3 - Add Document ID'
|
|
||||||
completed={state.id.complete}
|
|
||||||
disabled={!state.authenticate.complete}
|
|
||||||
error={state.id.message}
|
|
||||||
>
|
|
||||||
<HStack>
|
|
||||||
<Input
|
|
||||||
type='text'
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-filled-on-light'
|
|
||||||
isDisabled={!state.authenticate.complete}
|
|
||||||
value={id}
|
|
||||||
onChange={(event) => setSheetId(event.target.value)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
isDisabled={!state.authenticate.complete}
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-subtle-on-light'
|
|
||||||
padding='0 2em'
|
|
||||||
onClick={testSheetId}
|
|
||||||
>
|
|
||||||
Connect
|
|
||||||
</Button>
|
|
||||||
</HStack>
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step
|
|
||||||
title='4 - Select Worksheet to import'
|
|
||||||
completed={state.worksheet.complete}
|
|
||||||
disabled={worksheetOptions.length == 0}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
size='sm'
|
|
||||||
isDisabled={worksheetOptions.length == 0}
|
|
||||||
placeholder='Select a worksheet'
|
|
||||||
onChange={(event) => testWorksheet(event.target.value)}
|
|
||||||
value={worksheet}
|
|
||||||
>
|
|
||||||
{worksheetOptions.map((value) => (
|
|
||||||
<option key={value} value={value}>
|
|
||||||
{value}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step title='5 - Upload / Download rundown' completed={false} disabled={!state.worksheet.complete}>
|
|
||||||
<div style={{ display: 'flex', gap: '1em' }}>
|
|
||||||
<Button
|
|
||||||
isDisabled={!state.worksheet.complete}
|
|
||||||
variant='ontime-subtle-on-light'
|
|
||||||
padding='0 2em'
|
|
||||||
onClick={() => setDirection('up')}
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
isDisabled={!state.worksheet.complete}
|
|
||||||
variant='ontime-subtle-on-light'
|
|
||||||
padding='0 2em'
|
|
||||||
onClick={() => setDirection('down')}
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Step>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<PreviewExcel rundown={rundown ?? []} userFields={userFields ?? userFieldsPlaceholder} />
|
|
||||||
)}
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
{rundown ? (
|
|
||||||
<div style={{ display: 'flex', gap: '1em' }}>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setRundown(null);
|
|
||||||
setDirection('none');
|
|
||||||
}}
|
|
||||||
variant='ontime-ghost-on-light'
|
|
||||||
>
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
<Button variant='ontime-filled' padding='0 2em' onClick={handleFinalise}>
|
|
||||||
Import
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : direction === 'up' ? (
|
|
||||||
<div style={{ display: 'flex', gap: '1em' }}>
|
|
||||||
<Button onClick={() => setDirection('none')} variant='ontime-ghost-on-light'>
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
<Button variant='ontime-filled' padding='0 2em' onClick={handlePushData}>
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : direction === 'down' ? (
|
|
||||||
<div style={{ display: 'flex', gap: '1em' }}>
|
|
||||||
<Button onClick={() => setDirection('none')} variant='ontime-ghost-on-light'>
|
|
||||||
Go Back
|
|
||||||
</Button>
|
|
||||||
<Button variant='ontime-filled' padding='0 2em' onClick={handlePullData}>
|
|
||||||
Preview
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
.wrapper {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.step {
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
display: grid;
|
|
||||||
place-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: inline;
|
|
||||||
color: $gray-500;
|
|
||||||
text-transform: uppercase;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check {
|
|
||||||
color: $green-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorIcon {
|
|
||||||
color: $red-700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errorText {
|
|
||||||
color: $red-700;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { PropsWithChildren, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { IoCheckmarkCircle } from '@react-icons/all-files/io5/IoCheckmarkCircle';
|
|
||||||
import { IoCloseCircle } from '@react-icons/all-files/io5/IoCloseCircle';
|
|
||||||
import { IoRadioButtonOffOutline } from '@react-icons/all-files/io5/IoRadioButtonOffOutline';
|
|
||||||
|
|
||||||
import style from './Step.module.scss';
|
|
||||||
|
|
||||||
interface StepProps {
|
|
||||||
title: string;
|
|
||||||
disabled: boolean;
|
|
||||||
completed: boolean;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Step(props: PropsWithChildren<StepProps>) {
|
|
||||||
const { title, disabled, completed, error, children } = props;
|
|
||||||
const [collapsed, setCollapsed] = useState(disabled);
|
|
||||||
|
|
||||||
const handleCollapse = () => setCollapsed((prev) => !prev);
|
|
||||||
|
|
||||||
const icon = useMemo(() => {
|
|
||||||
if (completed) return <IoCheckmarkCircle className={style.step} style={{ color: 'green' }} />;
|
|
||||||
if (error) return <IoCloseCircle className={style.step} style={{ color: 'red' }} />;
|
|
||||||
return <IoRadioButtonOffOutline className={style.step} />;
|
|
||||||
}, [completed, error]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (completed) {
|
|
||||||
setCollapsed(true);
|
|
||||||
}
|
|
||||||
}, [completed]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.wrapper}>
|
|
||||||
<div className={style.header} onClick={handleCollapse}>
|
|
||||||
{icon}
|
|
||||||
<span className={style.title}>{title}</span>
|
|
||||||
</div>
|
|
||||||
{!collapsed && (
|
|
||||||
<>
|
|
||||||
{error && <div className={style.errorText}>{error}</div>}
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -207,7 +207,9 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
|||||||
<>
|
<>
|
||||||
<UploadFile />
|
<UploadFile />
|
||||||
{isOntime && <OntimeFileOptions optionsRef={ontimeFileOptions} updateOptions={updateOntimeFileOptions} />}
|
{isOntime && <OntimeFileOptions optionsRef={ontimeFileOptions} updateOptions={updateOntimeFileOptions} />}
|
||||||
{isExcel && <ExcelFileOptions optionsRef={excelFileOptions} updateOptions={updateExcelFileOptions} />}
|
{isExcel && (
|
||||||
|
<ExcelFileOptions importOptions={excelFileOptions.current} updateOptions={updateExcelFileOptions} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<PreviewExcel rundown={rundown ?? []} userFields={userFields ?? userFieldsPlaceholder} />
|
<PreviewExcel rundown={rundown ?? []} userFields={userFields ?? userFieldsPlaceholder} />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { MutableRefObject } from 'react';
|
|
||||||
import { ExcelImportMap } from 'ontime-utils';
|
import { ExcelImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import ImportMapTable, { type TableEntry } from './ImportMapTable';
|
import ImportMapTable, { type TableEntry } from './ImportMapTable';
|
||||||
@@ -6,50 +5,50 @@ import ImportMapTable, { type TableEntry } from './ImportMapTable';
|
|||||||
import style from '../UploadModal.module.scss';
|
import style from '../UploadModal.module.scss';
|
||||||
|
|
||||||
interface ExcelFileOptionsProps {
|
interface ExcelFileOptionsProps {
|
||||||
optionsRef: MutableRefObject<ExcelImportMap>;
|
importOptions: ExcelImportMap;
|
||||||
updateOptions: <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => void;
|
updateOptions: <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExcelFileOptions(props: ExcelFileOptionsProps) {
|
export default function ExcelFileOptions(props: ExcelFileOptionsProps) {
|
||||||
const { optionsRef, updateOptions } = props;
|
const { importOptions, updateOptions } = props;
|
||||||
|
|
||||||
const worksheet: TableEntry[] = [{ label: 'Worksheet', title: 'worksheet', value: optionsRef.current.worksheet }];
|
const worksheet: TableEntry[] = [{ label: 'Worksheet', title: 'worksheet', value: importOptions.worksheet }];
|
||||||
|
|
||||||
const timings: TableEntry[] = [
|
const timings: TableEntry[] = [
|
||||||
{ label: 'Start time', title: 'timeStart', value: optionsRef.current.timeStart },
|
{ label: 'Start time', title: 'timeStart', value: importOptions.timeStart },
|
||||||
{ label: 'End Time', title: 'timeEnd', value: optionsRef.current.timeEnd },
|
{ label: 'End Time', title: 'timeEnd', value: importOptions.timeEnd },
|
||||||
{ label: 'Duration', title: 'duration', value: optionsRef.current.duration },
|
{ label: 'Duration', title: 'duration', value: importOptions.duration },
|
||||||
{ label: 'Warning Time', title: 'timeWarning', value: optionsRef.current.timeWarning },
|
{ label: 'Warning Time', title: 'timeWarning', value: importOptions.timeWarning },
|
||||||
{ label: 'Danger Time', title: 'timeDanger', value: optionsRef.current.timeDanger },
|
{ label: 'Danger Time', title: 'timeDanger', value: importOptions.timeDanger },
|
||||||
];
|
];
|
||||||
|
|
||||||
const titles: TableEntry[] = [
|
const titles: TableEntry[] = [
|
||||||
{ label: 'Cue', title: 'cue', value: optionsRef.current.cue },
|
{ label: 'Cue', title: 'cue', value: importOptions.cue },
|
||||||
{ label: 'Colour', title: 'colour', value: optionsRef.current.colour },
|
{ label: 'Colour', title: 'colour', value: importOptions.colour },
|
||||||
{ label: 'Title', title: 'title', value: optionsRef.current.title },
|
{ label: 'Title', title: 'title', value: importOptions.title },
|
||||||
{ label: 'Presenter', title: 'presenter', value: optionsRef.current.presenter },
|
{ label: 'Presenter', title: 'presenter', value: importOptions.presenter },
|
||||||
{ label: 'Subtitle', title: 'subtitle', value: optionsRef.current.subtitle },
|
{ label: 'Subtitle', title: 'subtitle', value: importOptions.subtitle },
|
||||||
{ label: 'Note', title: 'note', value: optionsRef.current.note },
|
{ label: 'Note', title: 'note', value: importOptions.note },
|
||||||
];
|
];
|
||||||
|
|
||||||
const options: TableEntry[] = [
|
const options: TableEntry[] = [
|
||||||
{ label: 'Is Public', title: 'isPublic', value: optionsRef.current.isPublic },
|
{ label: 'Is Public', title: 'isPublic', value: importOptions.isPublic },
|
||||||
{ label: 'Skip', title: 'skip', value: optionsRef.current.skip },
|
{ label: 'Skip', title: 'skip', value: importOptions.skip },
|
||||||
{ label: 'Timer Type', title: 'timerType', value: optionsRef.current.timerType },
|
{ label: 'Timer Type', title: 'timerType', value: importOptions.timerType },
|
||||||
{ label: 'End Action', title: 'endAction', value: optionsRef.current.endAction },
|
{ label: 'End Action', title: 'endAction', value: importOptions.endAction },
|
||||||
];
|
];
|
||||||
|
|
||||||
const userFields: TableEntry[] = [
|
const userFields: TableEntry[] = [
|
||||||
{ label: 'User 0', title: 'user0', value: optionsRef.current.user0 },
|
{ label: 'User 0', title: 'user0', value: importOptions.user0 },
|
||||||
{ label: 'User 1', title: 'user1', value: optionsRef.current.user1 },
|
{ label: 'User 1', title: 'user1', value: importOptions.user1 },
|
||||||
{ label: 'User 2', title: 'user2', value: optionsRef.current.user2 },
|
{ label: 'User 2', title: 'user2', value: importOptions.user2 },
|
||||||
{ label: 'User 3', title: 'user3', value: optionsRef.current.user3 },
|
{ label: 'User 3', title: 'user3', value: importOptions.user3 },
|
||||||
{ label: 'User 4', title: 'user4', value: optionsRef.current.user4 },
|
{ label: 'User 4', title: 'user4', value: importOptions.user4 },
|
||||||
{ label: 'User 5', title: 'user5', value: optionsRef.current.user5 },
|
{ label: 'User 5', title: 'user5', value: importOptions.user5 },
|
||||||
{ label: 'User 6', title: 'user6', value: optionsRef.current.user6 },
|
{ label: 'User 6', title: 'user6', value: importOptions.user6 },
|
||||||
{ label: 'User 7', title: 'user7', value: optionsRef.current.user7 },
|
{ label: 'User 7', title: 'user7', value: importOptions.user7 },
|
||||||
{ label: 'User 8', title: 'user8', value: optionsRef.current.user8 },
|
{ label: 'User 8', title: 'user8', value: importOptions.user8 },
|
||||||
{ label: 'User 9', title: 'user9', value: optionsRef.current.user9 },
|
{ label: 'User 9', title: 'user9', value: importOptions.user9 },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ export default function ImportMapTable(props: ImportMapTableProps) {
|
|||||||
<td className={style.input}>
|
<td className={style.input}>
|
||||||
<Input
|
<Input
|
||||||
id={field.title}
|
id={field.title}
|
||||||
size='xs'
|
size='sm'
|
||||||
variant='ontime-filled-on-light'
|
variant='ontime-filled'
|
||||||
maxLength={25}
|
maxLength={25}
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
placeholder='Use default column name'
|
placeholder='Use default column name'
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ export const ontimeSelect = {
|
|||||||
color: '#f6f6f6', // $gray-50
|
color: '#f6f6f6', // $gray-50
|
||||||
border: '1px solid #578AF4', // $blue-500
|
border: '1px solid #578AF4', // $blue-500
|
||||||
},
|
},
|
||||||
|
_disabled: {
|
||||||
|
_hover: {
|
||||||
|
background: '#262626', // $gray-1100
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
color: '#e2e2e2', // $gray-200
|
color: '#e2e2e2', // $gray-200
|
||||||
|
|||||||
@@ -661,12 +661,13 @@ export async function uploadSheetClientFile(req, res: Response) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description STEP-1 GET Client Secrect status
|
* @description STEP-1 GET Client Secret status
|
||||||
*/
|
*/
|
||||||
export const getClientSecrect = async (req: Request, res: Response) => {
|
export const getClientSecret = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const clientSecrectExists = await sheet.testClientSecret();
|
// TODO: can we merge this with the previous?
|
||||||
if (clientSecrectExists) {
|
const clientSecretExists = await sheet.testClientSecret();
|
||||||
|
if (clientSecretExists) {
|
||||||
res.status(200).send();
|
res.status(200).send();
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send({ message: 'The Client ID does not exist' });
|
res.status(500).send({ message: 'The Client ID does not exist' });
|
||||||
@@ -706,11 +707,11 @@ export const getAuthentication = async (_req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const postId = async (req: Request, res: Response) => {
|
export const postId = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.body;
|
const { sheetId } = req.body;
|
||||||
if (id.lenght < 40) {
|
if (sheetId.length < 40) {
|
||||||
res.status(400).send({ message: 'ID is usualy 44 characters long' });
|
res.status(400).send({ message: 'ID is usually 44 characters long' });
|
||||||
}
|
}
|
||||||
const state = await sheet.testSheetId(id);
|
const state = await sheet.testSheetId(sheetId);
|
||||||
res.status(200).send(state);
|
res.status(200).send(state);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).send({ message: String(error) });
|
res.status(500).send({ message: String(error) });
|
||||||
@@ -722,8 +723,8 @@ export const postId = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const postWorksheet = async (req: Request, res: Response) => {
|
export const postWorksheet = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { worksheet, id } = req.body;
|
const { sheetId, worksheet } = req.body;
|
||||||
const state = await sheet.testWorksheet(worksheet, id);
|
const state = await sheet.testWorksheet(sheetId, worksheet);
|
||||||
res.status(200).send(state);
|
res.status(200).send(state);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).send({ message: String(error) });
|
res.status(500).send({ message: String(error) });
|
||||||
@@ -731,13 +732,15 @@ export const postWorksheet = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description STEP-5 POST download undown to sheet
|
* @description STEP-5 POST download rundown to sheet
|
||||||
* @returns parsed result
|
* @returns parsed result
|
||||||
*/
|
*/
|
||||||
export async function pullSheet(req: Request, res: Response) {
|
export async function pullSheet(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { id, options } = req.body;
|
const { sheetId, options } = req.body;
|
||||||
const data = await sheet.pull(id, options);
|
console.log('starting');
|
||||||
|
const data = await sheet.pull(sheetId, options);
|
||||||
|
console.log('finished');
|
||||||
res.status(200).send(data);
|
res.status(200).send(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).send({ message: String(error) });
|
res.status(500).send({ message: String(error) });
|
||||||
@@ -749,8 +752,8 @@ export async function pullSheet(req: Request, res: Response) {
|
|||||||
*/
|
*/
|
||||||
export async function pushSheet(req: Request, res: Response) {
|
export async function pushSheet(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { id, options } = req.body;
|
const { sheetId, options } = req.body;
|
||||||
await sheet.push(id, options);
|
await sheet.push(sheetId, options);
|
||||||
res.status(200).send();
|
res.status(200).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).send({ message: String(error) });
|
res.status(500).send({ message: String(error) });
|
||||||
|
|||||||
@@ -244,8 +244,8 @@ export const validateProjectFiles = (projectFiles: { filename?: string; newFilen
|
|||||||
return errors;
|
return errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateSheetid = [
|
export const validateSheetId = [
|
||||||
body('id').exists().isString(),
|
body('sheetId').exists().isString(),
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
@@ -255,7 +255,7 @@ export const validateSheetid = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const validateWorksheet = [
|
export const validateWorksheet = [
|
||||||
body('id').exists().isString(),
|
body('sheetId').exists().isString(),
|
||||||
body('worksheet').exists().isString(),
|
body('worksheet').exists().isString(),
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -266,7 +266,7 @@ export const validateWorksheet = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const validateSheetOptions = [
|
export const validateSheetOptions = [
|
||||||
body('id').exists().isString(),
|
body('sheetId').exists().isString(),
|
||||||
// body('options').exists().isObject(), TODO:
|
// body('options').exists().isObject(), TODO:
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ import {
|
|||||||
pushSheet,
|
pushSheet,
|
||||||
postId,
|
postId,
|
||||||
getAuthentication,
|
getAuthentication,
|
||||||
getClientSecrect as getClientSecret,
|
getClientSecret as getClientSecret,
|
||||||
|
postWorksheet,
|
||||||
} from '../controllers/ontimeController.js';
|
} from '../controllers/ontimeController.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -45,7 +46,7 @@ import {
|
|||||||
validateProjectDuplicate,
|
validateProjectDuplicate,
|
||||||
validateLoadProjectFile,
|
validateLoadProjectFile,
|
||||||
validateProjectRename,
|
validateProjectRename,
|
||||||
validateSheetid,
|
validateSheetId,
|
||||||
validateWorksheet,
|
validateWorksheet,
|
||||||
validateSheetOptions,
|
validateSheetOptions,
|
||||||
} from '../controllers/ontimeController.validate.js';
|
} from '../controllers/ontimeController.validate.js';
|
||||||
@@ -126,6 +127,7 @@ router.post('/project', projectSanitiser, createProjectFile);
|
|||||||
// create route between controller and '/ontime/project/:filename' endpoint
|
// create route between controller and '/ontime/project/:filename' endpoint
|
||||||
router.delete('/project/:filename', sanitizeProjectFilename, deleteProjectFile);
|
router.delete('/project/:filename', sanitizeProjectFilename, deleteProjectFile);
|
||||||
|
|
||||||
|
// TODO: move the google sheet stuff into a separate file
|
||||||
// Google Sheet integration - Step 1
|
// Google Sheet integration - Step 1
|
||||||
router.post('/sheet/clientsecret', uploadFile, uploadClientSecret);
|
router.post('/sheet/clientsecret', uploadFile, uploadClientSecret);
|
||||||
router.get('/sheet/clientsecret', uploadFile, getClientSecret);
|
router.get('/sheet/clientsecret', uploadFile, getClientSecret);
|
||||||
@@ -135,13 +137,13 @@ router.get('/sheet/authentication/url', getAuthenticationUrl);
|
|||||||
router.get('/sheet/authentication', getAuthentication);
|
router.get('/sheet/authentication', getAuthentication);
|
||||||
|
|
||||||
// Google Sheet integration - Step 3
|
// Google Sheet integration - Step 3
|
||||||
router.post('/sheet/id', validateSheetid, postId);
|
router.post('/sheet/sheetId', validateSheetId, postId);
|
||||||
|
|
||||||
// Google Sheet integration - Step 4
|
// Google Sheet integration - Step 4
|
||||||
router.post('/sheet/worksheet', validateWorksheet, postId);
|
router.post('/sheet/worksheet', validateWorksheet, postWorksheet);
|
||||||
|
|
||||||
// Google Sheet integration - Step 5
|
// Google Sheet integration - Step 5
|
||||||
router.post('/sheet/pull', validateSheetOptions, pullSheet);
|
router.post('/sheet-pull', validateSheetOptions, pullSheet);
|
||||||
|
|
||||||
// Google Sheet integration - Step 6
|
// Google Sheet integration - Step 6
|
||||||
router.post('/sheet-push', validateSheetOptions, pushSheet);
|
router.post('/sheet-push', validateSheetOptions, pushSheet);
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class Sheet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description STEP 1 - test that the saved object is pressent
|
* @description STEP 1 - test that the saved object is present
|
||||||
*/
|
*/
|
||||||
testClientSecret() {
|
testClientSecret() {
|
||||||
return Sheet.clientSecret !== null;
|
return Sheet.clientSecret !== null;
|
||||||
@@ -92,18 +92,15 @@ class Sheet {
|
|||||||
* @throws
|
* @throws
|
||||||
*/
|
*/
|
||||||
async openAuthServer(): Promise<string | null> {
|
async openAuthServer(): Promise<string | null> {
|
||||||
//TODO: this only works on local networks
|
//TODO: this only works in local networks
|
||||||
|
|
||||||
// if the server is allready running retun it
|
// if the server is already running return it
|
||||||
if (Sheet.authUrl) {
|
if (Sheet.authUrl) {
|
||||||
clearTimeout(this.authServerTimeout);
|
clearTimeout(this.authServerTimeout);
|
||||||
this.authServerTimeout = setTimeout(
|
this.authServerTimeout = setTimeout(() => {
|
||||||
() => {
|
|
||||||
Sheet.authUrl = null;
|
Sheet.authUrl = null;
|
||||||
server.unref();
|
server.unref();
|
||||||
},
|
}, 120000);
|
||||||
2 * 60 * 1000,
|
|
||||||
);
|
|
||||||
return Sheet.authUrl;
|
return Sheet.authUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,12 +206,12 @@ class Sheet {
|
|||||||
* @description STEP 3 - test the given sheet id
|
* @description STEP 3 - test the given sheet id
|
||||||
* @throws
|
* @throws
|
||||||
*/
|
*/
|
||||||
async testSheetId(id: string) {
|
async testSheetId(sheetId: string) {
|
||||||
const spreadsheets = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.get({
|
const spreadsheets = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.get({
|
||||||
spreadsheetId: id,
|
spreadsheetId: sheetId,
|
||||||
includeGridData: false,
|
includeGridData: false,
|
||||||
});
|
});
|
||||||
if (spreadsheets.status != 200) {
|
if (spreadsheets.status !== 200) {
|
||||||
throw new Error(spreadsheets.statusText);
|
throw new Error(spreadsheets.statusText);
|
||||||
}
|
}
|
||||||
return { worksheetOptions: spreadsheets.data.sheets.map((i) => i.properties.title) };
|
return { worksheetOptions: spreadsheets.data.sheets.map((i) => i.properties.title) };
|
||||||
@@ -224,12 +221,12 @@ class Sheet {
|
|||||||
* @description STEP 4 - test the given worksheet
|
* @description STEP 4 - test the given worksheet
|
||||||
* @throws
|
* @throws
|
||||||
*/
|
*/
|
||||||
async testWorksheet(id: string, worksheet: string) {
|
async testWorksheet(sheetId: string, worksheet: string) {
|
||||||
const spreadsheets = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.get({
|
const spreadsheets = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.get({
|
||||||
spreadsheetId: id,
|
spreadsheetId: sheetId,
|
||||||
includeGridData: false,
|
includeGridData: false,
|
||||||
});
|
});
|
||||||
if (spreadsheets.status != 200) {
|
if (spreadsheets.status !== 200) {
|
||||||
throw new Error(spreadsheets.statusText);
|
throw new Error(spreadsheets.statusText);
|
||||||
}
|
}
|
||||||
const worksheetExist = spreadsheets.data.sheets.find((i) => i.properties.title === worksheet);
|
const worksheetExist = spreadsheets.data.sheets.find((i) => i.properties.title === worksheet);
|
||||||
@@ -250,8 +247,12 @@ class Sheet {
|
|||||||
spreadsheetId: sheetId,
|
spreadsheetId: sheetId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (spreadsheets.status === 200) {
|
if (spreadsheets.status !== 200) {
|
||||||
|
throw new Error(`Request failed: ${spreadsheets.status} ${spreadsheets.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
const ourWorksheetData = spreadsheets.data.sheets.find((n) => n.properties.title == worksheet);
|
const ourWorksheetData = spreadsheets.data.sheets.find((n) => n.properties.title == worksheet);
|
||||||
|
|
||||||
if (ourWorksheetData !== undefined) {
|
if (ourWorksheetData !== undefined) {
|
||||||
const endCell = getA1Notation(
|
const endCell = getA1Notation(
|
||||||
ourWorksheetData.properties.gridProperties.rowCount,
|
ourWorksheetData.properties.gridProperties.rowCount,
|
||||||
@@ -259,9 +260,6 @@ class Sheet {
|
|||||||
);
|
);
|
||||||
return { worksheetId: ourWorksheetData.properties.sheetId, range: `${worksheet}!A1:${endCell}` };
|
return { worksheetId: ourWorksheetData.properties.sheetId, range: `${worksheet}!A1:${endCell}` };
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw new Error('Uable to open spreadsheets');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -302,7 +300,7 @@ class Sheet {
|
|||||||
updateRundown.push({
|
updateRundown.push({
|
||||||
deleteDimension: { range: { dimension: 'ROWS', startIndex: titleRow + 2, sheetId: worksheetId } },
|
deleteDimension: { range: { dimension: 'ROWS', startIndex: titleRow + 2, sheetId: worksheetId } },
|
||||||
});
|
});
|
||||||
// insert the lenght of the rundown
|
// insert the length of the rundown
|
||||||
updateRundown.push({
|
updateRundown.push({
|
||||||
insertDimension: {
|
insertDimension: {
|
||||||
inheritFromBefore: false,
|
inheritFromBefore: false,
|
||||||
@@ -340,19 +338,19 @@ class Sheet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description STEP 5 - Downpload the rundown from sheet
|
* @description STEP 5 - Download the rundown from sheet
|
||||||
* @param {string} id - id of the sheet https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0
|
* @param {string} sheetId - id of the sheet https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0
|
||||||
* @param {ExcelImportMap} options
|
* @param {ExcelImportMap} options
|
||||||
* @returns {Promise<Partial<ResponseOK>>}
|
* @returns {Promise<Partial<ResponseOK>>}
|
||||||
* @throws
|
* @throws
|
||||||
*/
|
*/
|
||||||
public async pull(id: string, options: ExcelImportMap): Promise<Partial<ResponseOK>> {
|
public async pull(sheetId: string, options: ExcelImportMap): Promise<Partial<ResponseOK>> {
|
||||||
const { range } = await this.exist(id, options.worksheet);
|
const { range } = await this.exist(sheetId, options.worksheet);
|
||||||
|
|
||||||
const res: Partial<ResponseOK> = {};
|
const res: Partial<ResponseOK> = {};
|
||||||
|
|
||||||
const googleResponse = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
|
const googleResponse = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
|
||||||
spreadsheetId: id,
|
spreadsheetId: sheetId,
|
||||||
valueRenderOption: 'FORMATTED_VALUE',
|
valueRenderOption: 'FORMATTED_VALUE',
|
||||||
majorDimension: 'ROWS',
|
majorDimension: 'ROWS',
|
||||||
range,
|
range,
|
||||||
|
|||||||
Reference in New Issue
Block a user