mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 13:23:35 +00:00
refactor: remove userFields (#791)
* refactor: process custom fields on cache generate * refactor: remove userFields
This commit is contained in:
@@ -30,6 +30,9 @@ $inner-padding: 1rem;
|
||||
margin-top: 2rem;
|
||||
font-size: calc(1rem - 1px);
|
||||
max-width: 800px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
@@ -60,15 +63,25 @@ $inner-padding: 1rem;
|
||||
border-collapse: collapse;
|
||||
font-size: calc(1rem - 2px);
|
||||
text-align: left;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 3;
|
||||
box-shadow: 0 1px $white-10;
|
||||
}
|
||||
|
||||
tr {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
th {
|
||||
border-bottom: 1px solid $white-10;
|
||||
font-weight: 400;
|
||||
color: $gray-400;
|
||||
background-color: $gray-1350;
|
||||
white-space: nowrap;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
th,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
import { cx } from '../../../common/utils/styleUtils';
|
||||
|
||||
import style from './Panel.module.scss';
|
||||
|
||||
export function Header({ children }: { children: ReactNode }) {
|
||||
@@ -41,10 +43,11 @@ export function Card({ children, ...props }: { children: ReactNode } & JSX.Intri
|
||||
);
|
||||
}
|
||||
|
||||
export function Table({ children }: { children: ReactNode }) {
|
||||
export function Table({ className, children }: { className?: string; children: ReactNode }) {
|
||||
const classes = cx([style.table, className]);
|
||||
return (
|
||||
<div className={style.pad}>
|
||||
<table className={style.table}>{children}</table>
|
||||
<table className={classes}>{children}</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function ProjectList({ isCreatingProject, onToggleCreate }: Proje
|
||||
<Panel.Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project Name</th>
|
||||
<th className={style.containCell}>Project Name</th>
|
||||
<th>Date Created</th>
|
||||
<th>Date Modified</th>
|
||||
<th />
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function ProjectListItem({
|
||||
</td>
|
||||
) : (
|
||||
<>
|
||||
<td>{filename}</td>
|
||||
<td className={style.containCell}>{filename}</td>
|
||||
<td>{new Date(createdAt).toLocaleString()}</td>
|
||||
<td>{new Date(updatedAt).toLocaleString()}</td>
|
||||
<td className={style.actionButton}>
|
||||
|
||||
@@ -54,4 +54,8 @@
|
||||
font-size: calc(1rem - 2px);
|
||||
font-weight: 400;
|
||||
color: $red-500;
|
||||
}
|
||||
}
|
||||
|
||||
.containCell {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import * as Panel from '../PanelUtils';
|
||||
import CustomFieldEntry from './CustomFieldEntry';
|
||||
import CustomFieldForm from './CustomFieldForm';
|
||||
|
||||
const userFieldsDocsUrl = 'https://ontime.gitbook.io/v2/features/user-fields';
|
||||
const customFieldsDocsUrl = 'https://ontime.gitbook.io/v2/features/user-fields';
|
||||
|
||||
export default function ProjectSettingsPanel() {
|
||||
const { data, refetch } = useCustomFields();
|
||||
@@ -64,7 +64,7 @@ export default function ProjectSettingsPanel() {
|
||||
<br />
|
||||
<br />
|
||||
This data is not used by Ontime.
|
||||
<ExternalLink href={userFieldsDocsUrl}>See the docs</ExternalLink>
|
||||
<ExternalLink href={customFieldsDocsUrl}>See the docs</ExternalLink>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</Panel.Section>
|
||||
|
||||
@@ -16,7 +16,9 @@ interface GSheetSetupProps {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function GSheetSetup({ onCancel }: GSheetSetupProps) {
|
||||
export default function GSheetSetup(props: GSheetSetupProps) {
|
||||
const { onCancel } = props;
|
||||
|
||||
const { revoke, connect, verifyAuth } = useGoogleSheet();
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [authKey, setAuthKey] = useState<string | null>(null);
|
||||
@@ -34,6 +36,10 @@ export default function GSheetSetup({ onCancel }: GSheetSetupProps) {
|
||||
const result = await verifyAuth();
|
||||
if (result) {
|
||||
setAuthenticationStatus(result.authenticated);
|
||||
// if we are still pending, lets check again in 2seconds
|
||||
if (result.authenticated === 'pending') {
|
||||
setTimeout(getAuthStatus, 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,11 +48,6 @@ export default function GSheetSetup({ onCancel }: GSheetSetupProps) {
|
||||
getAuthStatus();
|
||||
}, []);
|
||||
|
||||
const handleCancelFlow = () => {
|
||||
revoke();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
// user cancels the flow
|
||||
const handleRevoke = async () => {
|
||||
setLoading('cancel');
|
||||
@@ -55,6 +56,11 @@ export default function GSheetSetup({ onCancel }: GSheetSetupProps) {
|
||||
setLoading('');
|
||||
};
|
||||
|
||||
const handleCancelFlow = async () => {
|
||||
await handleRevoke();
|
||||
onCancel();
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets file from input
|
||||
* @param event
|
||||
@@ -104,85 +110,79 @@ export default function GSheetSetup({ onCancel }: GSheetSetupProps) {
|
||||
const canAuthenticate = Boolean(authKey) && Boolean(authLink);
|
||||
const isLoading = Boolean(loading);
|
||||
const isAuthenticated = authenticationStatus === 'authenticated';
|
||||
const isAuthenticating = authenticationStatus === 'pending';
|
||||
|
||||
return (
|
||||
<Panel.Section>
|
||||
<Panel.Title>
|
||||
Sync with Google Sheet (experimental)
|
||||
<Button variant='ontime-subtle' size='sm' onClick={handleCancelFlow}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Panel.Title>
|
||||
{isAuthenticated ? (
|
||||
<Panel.ListGroup>
|
||||
<Panel.Title>Authenticated</Panel.Title>
|
||||
{isAuthenticated ? (
|
||||
<Button variant='ontime-subtle' size='sm' onClick={handleRevoke} isLoading={loading === 'cancel'}>
|
||||
Revoke Authentication
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant='ontime-subtle' size='sm' onClick={handleCancelFlow}>
|
||||
Go Back
|
||||
</Button>
|
||||
)}
|
||||
</Panel.Title>
|
||||
<Panel.ListGroup>
|
||||
<Panel.Description>Upload Client Secret provided by Google</Panel.Description>
|
||||
<Panel.Error>{undefined}</Panel.Error>
|
||||
<Input
|
||||
type='file'
|
||||
onChange={handleClientSecret}
|
||||
accept='.json'
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
isDisabled={isLoading || canAuthenticate}
|
||||
/>
|
||||
</Panel.ListGroup>
|
||||
<Panel.ListGroup>
|
||||
<Panel.Description>Enter ID of sheet to synchronise</Panel.Description>
|
||||
<Panel.Error>{undefined}</Panel.Error>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
autoComplete='off'
|
||||
placeholder='Sheet ID'
|
||||
onChange={(event) => setSheetId(event.target.value)}
|
||||
isDisabled={isLoading || canAuthenticate}
|
||||
/>
|
||||
</Panel.ListGroup>
|
||||
{!canAuthenticate ? (
|
||||
<Panel.ListGroup>
|
||||
<div className={style.buttonRow}>
|
||||
<Button
|
||||
variant='ontime-subtle'
|
||||
size='sm'
|
||||
leftIcon={<IoCheckmark />}
|
||||
onClick={handleConnect}
|
||||
isDisabled={!canConnect || isLoading}
|
||||
isLoading={loading === 'connect'}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.ListGroup>
|
||||
) : (
|
||||
<>
|
||||
<Panel.ListGroup>
|
||||
<Panel.Description>Upload Client Secret provided by Google</Panel.Description>
|
||||
<Panel.Error>{undefined}</Panel.Error>
|
||||
<Input
|
||||
type='file'
|
||||
onChange={handleClientSecret}
|
||||
accept='.json'
|
||||
size='sm'
|
||||
<Panel.ListGroup>
|
||||
<div className={style.buttonRow}>
|
||||
<CopyTag label='Google Auth Key' disabled={!canAuthenticate} size='sm'>
|
||||
{authKey ? authKey : 'Upload files to generate Auth Key'}
|
||||
</CopyTag>
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
isDisabled={isLoading || canAuthenticate}
|
||||
/>
|
||||
</Panel.ListGroup>
|
||||
|
||||
<Panel.ListGroup>
|
||||
<Panel.Description>Enter ID of sheet to synchronise</Panel.Description>
|
||||
<Panel.Error>{undefined}</Panel.Error>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
autoComplete='off'
|
||||
placeholder='Sheet ID'
|
||||
onChange={(event) => setSheetId(event.target.value)}
|
||||
isDisabled={isLoading || canAuthenticate}
|
||||
/>
|
||||
</Panel.ListGroup>
|
||||
|
||||
{!canAuthenticate ? (
|
||||
<Panel.ListGroup>
|
||||
<div className={style.buttonRow}>
|
||||
<Button
|
||||
variant='ontime-subtle'
|
||||
size='sm'
|
||||
leftIcon={<IoCheckmark />}
|
||||
onClick={handleConnect}
|
||||
isDisabled={!canConnect || isLoading}
|
||||
isLoading={loading === 'connect'}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.ListGroup>
|
||||
) : (
|
||||
<Panel.ListGroup>
|
||||
<div className={style.buttonRow}>
|
||||
<CopyTag label='Google Auth Key' disabled={!canAuthenticate} size='sm'>
|
||||
{authKey ? authKey : 'Upload files to generate Auth Key'}
|
||||
</CopyTag>
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
leftIcon={<IoShieldCheckmarkOutline />}
|
||||
onClick={handleAuthenticate}
|
||||
isDisabled={!canAuthenticate || isLoading}
|
||||
isLoading={loading === 'authenticate'}
|
||||
>
|
||||
Authenticate
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.ListGroup>
|
||||
)}
|
||||
</>
|
||||
leftIcon={<IoShieldCheckmarkOutline />}
|
||||
onClick={handleAuthenticate}
|
||||
isDisabled={!canAuthenticate || isLoading}
|
||||
isLoading={loading === 'authenticate' || isAuthenticating}
|
||||
>
|
||||
Authenticate
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.ListGroup>
|
||||
)}
|
||||
</Panel.Section>
|
||||
);
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
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 { importRundownPreview, exportRundown } = useGoogleSheet();
|
||||
|
||||
const importOptions = useSheetStore((state) => state.excelFileOptions);
|
||||
const patchImportOptions = useSheetStore((state) => state.patchExcelFileOptions);
|
||||
const stepData = useSheetStore((state) => state.stepData);
|
||||
const sheetId = useSheetStore((state) => state.sheetId);
|
||||
|
||||
const [loading, setLoading] = useState<'' | 'export' | 'import'>('');
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!sheetId) return;
|
||||
setLoading('export');
|
||||
await exportRundown(sheetId, importOptions);
|
||||
setLoading('');
|
||||
};
|
||||
|
||||
const handleImportPreview = async () => {
|
||||
if (!sheetId) return;
|
||||
setLoading('import');
|
||||
await importRundownPreview(sheetId, importOptions);
|
||||
setLoading('');
|
||||
};
|
||||
|
||||
const isLoading = Boolean(loading);
|
||||
|
||||
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={handleExport}
|
||||
isDisabled={isLoading || !sheetId}
|
||||
isLoading={loading === 'export'}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
onClick={handleImportPreview}
|
||||
isDisabled={isLoading || !sheetId}
|
||||
isLoading={loading === 'import'}
|
||||
>
|
||||
Import preview
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.Section>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import { OntimeRundown, UserFields } from 'ontime-types';
|
||||
import { CustomFields, OntimeRundown } from 'ontime-types';
|
||||
|
||||
import PreviewExcel from '../../../modals/upload-modal/preview/PreviewExcel';
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import PreviewSpreadsheet from './preview/PreviewRundown';
|
||||
import useGoogleSheet from './useGoogleSheet';
|
||||
import { useSheetStore } from './useSheetStore';
|
||||
|
||||
@@ -10,28 +12,43 @@ import style from './SourcesPanel.module.scss';
|
||||
|
||||
interface ImportReviewProps {
|
||||
rundown: OntimeRundown;
|
||||
userFields: UserFields;
|
||||
customFields: CustomFields;
|
||||
onFinished: () => void;
|
||||
}
|
||||
|
||||
export default function ImportReview({ rundown, userFields }: ImportReviewProps) {
|
||||
export default function ImportReview(props: ImportReviewProps) {
|
||||
const { rundown, customFields, onFinished } = props;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { importRundown } = useGoogleSheet();
|
||||
const resetPreview = useSheetStore((state) => state.resetPreview);
|
||||
|
||||
const applyImport = () => {
|
||||
importRundown(rundown, userFields);
|
||||
const handleCancel = () => {
|
||||
resetPreview();
|
||||
onFinished();
|
||||
};
|
||||
|
||||
const applyImport = async () => {
|
||||
setLoading(true);
|
||||
await importRundown(rundown, customFields);
|
||||
setLoading(false);
|
||||
onFinished();
|
||||
};
|
||||
|
||||
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>
|
||||
</>
|
||||
<Panel.Section>
|
||||
<Panel.Title>
|
||||
Review Rundown
|
||||
<div className={style.buttonRow}>
|
||||
<Button onClick={handleCancel} variant='ontime-ghosted' size='sm' isDisabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={applyImport} variant='ontime-filled' size='sm' isLoading={loading}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.Title>
|
||||
<PreviewSpreadsheet rundown={rundown} customFields={customFields} />
|
||||
</Panel.Section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,3 +20,8 @@
|
||||
.inputContainer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.singleActionCell {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,61 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { ChangeEvent, 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 { ImportMap, unpackError } from 'ontime-utils';
|
||||
|
||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
||||
import { importSpreadsheetPreview } from '../../../../common/api/ontimeApi';
|
||||
import { validateSpreadsheetImport } from '../../../../common/utils/uploadUtils';
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import ImportMapForm from './import-map/ImportMapForm';
|
||||
import GSheetInfo from './GSheetInfo';
|
||||
import GSheetSetup from './GSheetSetup';
|
||||
import ImportMap from './ImportMap';
|
||||
import ImportReview from './ImportReview';
|
||||
import useGoogleSheet from './useGoogleSheet';
|
||||
import { useSheetStore } from './useSheetStore';
|
||||
|
||||
import style from './SourcesPanel.module.scss';
|
||||
|
||||
export default function SourcesPanel() {
|
||||
const [importFlow, setImportFlow] = useState<'none' | 'excel' | 'gsheet'>('none');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const { exportRundown, importRundownPreview, revoke, verifyAuth } = useGoogleSheet();
|
||||
|
||||
const spreadsheet = useSheetStore((state) => state.spreadsheet);
|
||||
const setSpreadsheet = useSheetStore((state) => state.setSpreadsheet);
|
||||
const authenticationStatus = useSheetStore((state) => state.authenticationStatus);
|
||||
const setAuthenticationStatus = useSheetStore((state) => state.setAuthenticationStatus);
|
||||
const rundown = useSheetStore((state) => state.rundown);
|
||||
const userFields = useSheetStore((state) => state.userFields);
|
||||
|
||||
const isAuthenticated = authenticationStatus === 'authenticated';
|
||||
const hasData = rundown && userFields;
|
||||
const setRundown = useSheetStore((state) => state.setRundown);
|
||||
const customFields = useSheetStore((state) => state.customFields);
|
||||
const setCustomFields = useSheetStore((state) => state.setCustomFields);
|
||||
const sheetId = useSheetStore((state) => state.sheetId);
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFile = () => console.error('not yet implemented');
|
||||
const handleFile = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const fileToUpload = event.target.files?.[0];
|
||||
|
||||
if (!fileToUpload) {
|
||||
setSpreadsheet(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validateSpreadsheetImport(fileToUpload);
|
||||
setSpreadsheet(fileToUpload);
|
||||
setImportFlow('excel');
|
||||
} catch (error) {
|
||||
const errorMessage = unpackError(error);
|
||||
setError(`Error uploading file: ${errorMessage}`);
|
||||
setSpreadsheet(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = () => {
|
||||
fileInputRef.current?.click();
|
||||
setImportFlow('excel');
|
||||
};
|
||||
|
||||
const openGSheetFlow = () => {
|
||||
@@ -40,28 +66,81 @@ export default function SourcesPanel() {
|
||||
setImportFlow('none');
|
||||
};
|
||||
|
||||
const handleSubmitImportPreview = async (importMap: ImportMap) => {
|
||||
if (importFlow === 'excel') {
|
||||
if (!spreadsheet) return;
|
||||
try {
|
||||
const previewData = await importSpreadsheetPreview(spreadsheet, importMap);
|
||||
setRundown(previewData.rundown);
|
||||
setCustomFields(previewData.customFields);
|
||||
} catch (error) {
|
||||
setError(maybeAxiosError(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (importFlow === 'gsheet') {
|
||||
if (!sheetId) return;
|
||||
await importRundownPreview(sheetId, importMap);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelImportMap = async () => {
|
||||
setImportFlow('none');
|
||||
if (spreadsheet) {
|
||||
setSpreadsheet(null);
|
||||
}
|
||||
|
||||
if (authenticationStatus === 'authenticated') {
|
||||
await revoke();
|
||||
const result = await verifyAuth();
|
||||
if (result) {
|
||||
setAuthenticationStatus(result.authenticated);
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleFinished = () => {
|
||||
setImportFlow('none');
|
||||
setRundown(null);
|
||||
setSpreadsheet(null);
|
||||
setCustomFields(null);
|
||||
};
|
||||
|
||||
const handleSubmitExport = async (importMap: ImportMap) => {
|
||||
if (!sheetId) return;
|
||||
await exportRundown(sheetId, importMap);
|
||||
};
|
||||
|
||||
const isExcelFlow = importFlow === 'excel';
|
||||
const isGSheetFlow = importFlow === 'gsheet';
|
||||
const hasFile = Boolean(spreadsheet);
|
||||
const isAuthenticated = authenticationStatus === 'authenticated';
|
||||
const showInput = importFlow === 'none';
|
||||
const showAuth = isGSheetFlow && !isAuthenticated;
|
||||
const showImportMap = (isGSheetFlow && isAuthenticated) || (isExcelFlow && hasFile);
|
||||
const showReview = rundown !== null && customFields !== null;
|
||||
|
||||
console.log(isAuthenticated);
|
||||
return (
|
||||
<>
|
||||
<Panel.Header>Data sources</Panel.Header>
|
||||
<Panel.Section>
|
||||
<Panel.Card>
|
||||
<Panel.SubHeader>Synchronise your rundown with an external source</Panel.SubHeader>
|
||||
<GSheetInfo />
|
||||
{!isGSheetFlow && (
|
||||
{error && <Panel.Error>{error}</Panel.Error>}
|
||||
{showInput && (
|
||||
<>
|
||||
<Input ref={fileInputRef} style={{ display: 'none' }} type='file' onChange={handleFile} accept='.xlsx' />
|
||||
<GSheetInfo />
|
||||
<Input
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
type='file'
|
||||
onChange={handleFile}
|
||||
accept='.xlsx'
|
||||
data-testid='file-input'
|
||||
/>
|
||||
<div className={style.uploadSection}>
|
||||
<div>
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
leftIcon={<IoDownloadOutline />}
|
||||
onClick={handleUpload}
|
||||
isDisabled
|
||||
>
|
||||
<Button variant='ontime-filled' size='sm' leftIcon={<IoDownloadOutline />} onClick={handleUpload}>
|
||||
Import from spreadsheet
|
||||
</Button>
|
||||
<Panel.Description>Accepts .xlsx files</Panel.Description>
|
||||
@@ -75,10 +154,16 @@ export default function SourcesPanel() {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isGSheetFlow && <GSheetSetup onCancel={cancelGSheetFlow} />}
|
||||
{isExcelFlow && <Panel.Title>Not yet implemented</Panel.Title>}
|
||||
{isAuthenticated && <ImportMap />}
|
||||
{hasData && <ImportReview rundown={rundown} userFields={userFields} />}
|
||||
{showAuth && <GSheetSetup onCancel={cancelGSheetFlow} />}
|
||||
{showImportMap && !showReview && (
|
||||
<ImportMapForm
|
||||
isSpreadsheet={isExcelFlow}
|
||||
onCancel={cancelImportMap}
|
||||
onSubmitExport={handleSubmitExport}
|
||||
onSubmitImport={handleSubmitImportPreview}
|
||||
/>
|
||||
)}
|
||||
{showReview && <ImportReview rundown={rundown} customFields={customFields} onFinished={handleFinished} />}
|
||||
</Panel.Card>
|
||||
</Panel.Section>
|
||||
</>
|
||||
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
import { useState } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { Button, IconButton, Input } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { isAlphanumeric } from '../../../../../common/utils/regex';
|
||||
import * as Panel from '../../PanelUtils';
|
||||
import { useSheetStore } from '../useSheetStore';
|
||||
|
||||
import { convertToImportMap, getPersistedOptions, NamedImportMap, persistImportMap } from './importMapUtils';
|
||||
|
||||
import style from '../SourcesPanel.module.scss';
|
||||
|
||||
interface ImportMapFormProps {
|
||||
isSpreadsheet?: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmitExport: (importMap: ImportMap) => Promise<void>;
|
||||
onSubmitImport: (importMap: ImportMap) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function ImportMapForm(props: ImportMapFormProps) {
|
||||
const { isSpreadsheet, onCancel, onSubmitExport, onSubmitImport } = props;
|
||||
const namedImportMap = getPersistedOptions();
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
formState: { errors, isValid },
|
||||
} = useForm<NamedImportMap>({
|
||||
mode: 'onBlur',
|
||||
defaultValues: namedImportMap,
|
||||
values: namedImportMap,
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: 'custom',
|
||||
});
|
||||
|
||||
const stepData = useSheetStore((state) => state.stepData);
|
||||
|
||||
const [loading, setLoading] = useState<'' | 'export' | 'import'>('');
|
||||
|
||||
const handleExport = async (values: NamedImportMap) => {
|
||||
setLoading('export');
|
||||
const importMap = convertToImportMap(values);
|
||||
|
||||
await onSubmitExport(importMap);
|
||||
setLoading('');
|
||||
};
|
||||
|
||||
const handleImportPreview = async (values: NamedImportMap) => {
|
||||
setLoading('import');
|
||||
const importMap = convertToImportMap(values);
|
||||
persistImportMap(values);
|
||||
await onSubmitImport(importMap);
|
||||
setLoading('');
|
||||
};
|
||||
|
||||
const deleteCustomImport = (index: number) => {
|
||||
remove(index);
|
||||
};
|
||||
|
||||
const addCustomImport = () => {
|
||||
append({});
|
||||
};
|
||||
|
||||
const isLoading = Boolean(loading);
|
||||
const canSubmitSpreadsheet = isSpreadsheet && !isLoading;
|
||||
const canSubmitGSheet = !isLoading;
|
||||
const canSubmit = isValid && (canSubmitSpreadsheet || canSubmitGSheet);
|
||||
|
||||
return (
|
||||
<Panel.Section as='form' id='import-map'>
|
||||
<Panel.Title>
|
||||
Import options
|
||||
<div className={style.buttonRow}>
|
||||
<Button variant='ontime-subtle' size='sm' onClick={onCancel} isDisabled={isLoading}>
|
||||
Cancel
|
||||
</Button>
|
||||
{!isSpreadsheet && (
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
onClick={handleSubmit(handleExport)}
|
||||
isDisabled={!canSubmitGSheet}
|
||||
isLoading={loading === 'export'}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant='ontime-filled'
|
||||
size='sm'
|
||||
onClick={handleSubmit(handleImportPreview)}
|
||||
isDisabled={!canSubmit}
|
||||
isLoading={loading === 'import'}
|
||||
>
|
||||
Import preview
|
||||
</Button>
|
||||
</div>
|
||||
</Panel.Title>
|
||||
<Panel.Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ontime field</th>
|
||||
<th>From spreadsheet name</th>
|
||||
<th className={style.singleActionCell} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Object.entries(namedImportMap).map(([label, importName]) => {
|
||||
if (label === 'custom') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<tr key={importName as string}>
|
||||
<td>{label}</td>
|
||||
<td>
|
||||
<Input
|
||||
id={importName as string}
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
autoComplete='off'
|
||||
maxLength={25}
|
||||
defaultValue={importName as string}
|
||||
placeholder='Use default column name'
|
||||
{...register(label as keyof NamedImportMap)}
|
||||
/>
|
||||
</td>
|
||||
<td className={style.singleActionCell} />
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{fields.map((field, index) => {
|
||||
const ontimeName = field.ontimeName;
|
||||
const importName = field.importName;
|
||||
const maybeOntimeError = errors.custom?.[index]?.ontimeName?.message;
|
||||
const key = `custom.${index}.ontimeName`;
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
autoComplete='off'
|
||||
maxLength={25}
|
||||
defaultValue={ontimeName}
|
||||
placeholder='Name of the field as shown in Ontime'
|
||||
{...register(`custom.${index}.ontimeName`, {
|
||||
pattern: {
|
||||
value: isAlphanumeric,
|
||||
message: 'Custom field name must be alphanumeric',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{maybeOntimeError && <Panel.Error>{maybeOntimeError}</Panel.Error>}
|
||||
</td>
|
||||
<td>
|
||||
<Input
|
||||
size='sm'
|
||||
variant='ontime-filled'
|
||||
autoComplete='off'
|
||||
maxLength={25}
|
||||
defaultValue={importName}
|
||||
placeholder='Name of the column in the spreadsheet'
|
||||
{...register(`custom.${index}.importName`)}
|
||||
/>
|
||||
</td>
|
||||
<td className={style.singleActionCell}>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='ontime-ghosted'
|
||||
color='#FA5656' // $red-500
|
||||
icon={<IoTrash />}
|
||||
aria-label='Delete entry'
|
||||
onClick={() => deleteCustomImport(index)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
<tr>
|
||||
<td />
|
||||
<td className={style.buttonRow} colSpan={99}>
|
||||
<Button size='sm' variant='ontime-subtle' rightIcon={<IoAdd />} onClick={addCustomImport}>
|
||||
Add custom field
|
||||
</Button>
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
</tbody>
|
||||
</Panel.Table>
|
||||
<Panel.Error>{stepData.worksheet.error}</Panel.Error>
|
||||
</Panel.Section>
|
||||
);
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { ImportCustom } from 'ontime-utils';
|
||||
|
||||
import { convertToImportMap } from '../importMapUtils';
|
||||
|
||||
describe('convertToImportMap', () => {
|
||||
it('converts a namedImportMap to a importMap', () => {
|
||||
const defaultNamedImporMap = {
|
||||
Worksheet: 'event schedule',
|
||||
Start: 'time start',
|
||||
End: 'time end',
|
||||
Duration: 'duration',
|
||||
Cue: 'cue',
|
||||
Title: 'title',
|
||||
Presenter: 'presenter',
|
||||
Subtitle: 'subtitle',
|
||||
'Is Public': 'public',
|
||||
Skip: 'skip',
|
||||
Note: 'notes',
|
||||
Colour: 'colour',
|
||||
'End action': 'end action',
|
||||
'Timer type': 'timer type',
|
||||
'Time warning': 'warning time',
|
||||
'Time danger': 'danger time',
|
||||
custom: [
|
||||
{ ontimeName: 'Custom1 ', importName: 'custom1' },
|
||||
{ ontimeName: 'Custom2', importName: 'custom2' },
|
||||
{ ontimeName: 'Custom3', importName: 'custom3' },
|
||||
{ ontimeName: 'EmptyImportName', importName: '' },
|
||||
{ ontimeName: '', importName: 'EmptyOntimeName' },
|
||||
] as ImportCustom[],
|
||||
};
|
||||
|
||||
const importMap = convertToImportMap(defaultNamedImporMap);
|
||||
expect(importMap.custom).toStrictEqual({
|
||||
Custom1: 'custom1',
|
||||
Custom2: 'custom2',
|
||||
Custom3: 'custom3',
|
||||
});
|
||||
});
|
||||
});
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { ImportCustom, ImportMap } from 'ontime-utils';
|
||||
|
||||
export type NamedImportMap = typeof namedImportMap;
|
||||
|
||||
// Record of label and import name
|
||||
export const namedImportMap = {
|
||||
Worksheet: 'event schedule',
|
||||
Start: 'time start',
|
||||
End: 'time end',
|
||||
Duration: 'duration',
|
||||
Cue: 'cue',
|
||||
Title: 'title',
|
||||
Presenter: 'presenter',
|
||||
Subtitle: 'subtitle',
|
||||
'Is Public': 'public',
|
||||
Skip: 'skip',
|
||||
Note: 'notes',
|
||||
Colour: 'colour',
|
||||
'End action': 'end action',
|
||||
'Timer type': 'timer type',
|
||||
'Time warning': 'warning time',
|
||||
'Time danger': 'danger time',
|
||||
custom: [] as ImportCustom[],
|
||||
};
|
||||
|
||||
export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap {
|
||||
const custom = namedImportMap.custom.reduce((accumulator, { ontimeName, importName }) => {
|
||||
if (ontimeName && importName) {
|
||||
accumulator[ontimeName.trim()] = importName.trim();
|
||||
}
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
worksheet: namedImportMap.Worksheet,
|
||||
timeStart: namedImportMap.Start,
|
||||
timeEnd: namedImportMap.End,
|
||||
duration: namedImportMap.Duration,
|
||||
cue: namedImportMap.Cue,
|
||||
title: namedImportMap.Title,
|
||||
presenter: namedImportMap.Presenter,
|
||||
subtitle: namedImportMap.Subtitle,
|
||||
isPublic: namedImportMap['Is Public'],
|
||||
skip: namedImportMap.Skip,
|
||||
note: namedImportMap.Note,
|
||||
colour: namedImportMap.Colour,
|
||||
endAction: namedImportMap['End action'],
|
||||
timerType: namedImportMap['Timer type'],
|
||||
timeWarning: namedImportMap['Time warning'],
|
||||
timeDanger: namedImportMap['Time danger'],
|
||||
custom,
|
||||
};
|
||||
}
|
||||
|
||||
export function persistImportMap(options: NamedImportMap) {
|
||||
localStorage.setItem('ontime-import-options', JSON.stringify(options));
|
||||
}
|
||||
|
||||
export function getPersistedOptions(): NamedImportMap {
|
||||
const options = localStorage.getItem('ontime-import-options');
|
||||
if (!options) {
|
||||
return namedImportMap;
|
||||
}
|
||||
return JSON.parse(options);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tr .secondaryRow {
|
||||
background-color: $white-7;
|
||||
padding-left: 2em;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Fragment } from 'react';
|
||||
import { CustomFields, isOntimeBlock, isOntimeEvent, OntimeRundown } from 'ontime-types';
|
||||
import { millisToString } from 'ontime-utils';
|
||||
|
||||
import Tag from '../../../../../common/components/tag/Tag';
|
||||
import { getAccessibleColour } from '../../../../../common/utils/styleUtils';
|
||||
import * as Panel from '../../PanelUtils';
|
||||
|
||||
import style from './PreviewRundown.module.scss';
|
||||
|
||||
interface PreviewRundownProps {
|
||||
rundown: OntimeRundown;
|
||||
customFields: CustomFields;
|
||||
}
|
||||
|
||||
function booleanToText(value?: boolean) {
|
||||
return value ? 'Yes' : undefined;
|
||||
}
|
||||
|
||||
export default function PreviewRundown(props: PreviewRundownProps) {
|
||||
const { rundown, customFields } = props;
|
||||
|
||||
// we only count Ontime Events which are 1 based in client
|
||||
let eventIndex = 0;
|
||||
|
||||
const fieldHeaders = Object.keys(customFields);
|
||||
|
||||
return (
|
||||
<Panel.Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Type</th>
|
||||
<th>Cue</th>
|
||||
<th>Title</th>
|
||||
<th>Subtitle</th>
|
||||
<th>Presenter</th>
|
||||
<th>Time Start</th>
|
||||
<th>Time End</th>
|
||||
<th>Duration</th>
|
||||
<th>Warning Time</th>
|
||||
<th>Danger Time</th>
|
||||
<th>Is Public</th>
|
||||
<th>Skip</th>
|
||||
<th>Colour</th>
|
||||
<th>Timer Type</th>
|
||||
<th>End Action</th>
|
||||
{fieldHeaders.map((field) => (
|
||||
<th key={field}>{field}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rundown.map((event) => {
|
||||
if (isOntimeBlock(event)) {
|
||||
return (
|
||||
<tr key={event.id}>
|
||||
<td className={style.center}>
|
||||
<Tag>-</Tag>
|
||||
</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{event.type}</Tag>
|
||||
</td>
|
||||
<td />
|
||||
<td colSpan={99}>{event.title}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
if (!isOntimeEvent(event)) {
|
||||
return null;
|
||||
}
|
||||
eventIndex += 1;
|
||||
const colour = event.colour ? getAccessibleColour(event.colour) : {};
|
||||
const isPublic = booleanToText(event.isPublic);
|
||||
const skip = booleanToText(event.skip);
|
||||
|
||||
return (
|
||||
<Fragment key={event.id}>
|
||||
<tr>
|
||||
<td className={style.center}>
|
||||
<Tag>{eventIndex}</Tag>
|
||||
</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{event.type}</Tag>
|
||||
</td>
|
||||
<td className={style.nowrap}>{event.cue}</td>
|
||||
<td>{event.title}</td>
|
||||
<td>{event.subtitle}</td>
|
||||
<td>{event.presenter}</td>
|
||||
<td>{millisToString(event.timeStart)}</td>
|
||||
<td>{millisToString(event.timeEnd)}</td>
|
||||
<td>{millisToString(event.duration)}</td>
|
||||
<td>{millisToString(event.timeWarning)}</td>
|
||||
<td>{millisToString(event.timeDanger)}</td>
|
||||
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
|
||||
<td>{skip && <Tag>{skip}</Tag>}</td>
|
||||
<td style={{ ...colour }}>{event.colour}</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{event.timerType}</Tag>
|
||||
</td>
|
||||
<td className={style.center}>
|
||||
<Tag>{event.endAction}</Tag>
|
||||
</td>
|
||||
{isOntimeEvent(event) &&
|
||||
fieldHeaders.map((field) => {
|
||||
let value = '';
|
||||
if (field in event.custom) {
|
||||
value = event.custom[field].value;
|
||||
}
|
||||
return <td key={field}>{value}</td>;
|
||||
})}
|
||||
</tr>
|
||||
{event.note && (
|
||||
<tr>
|
||||
<td colSpan={99} className={style.secondaryRow}>
|
||||
Note: {event.note}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Panel.Table>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { AuthenticationStatus, OntimeRundown, UserFields } from 'ontime-types';
|
||||
import { ExcelImportMap } from 'ontime-utils';
|
||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { RUNDOWN, USERFIELDS } from '../../../../common/api/apiConstants';
|
||||
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/apiConstants';
|
||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
||||
import {
|
||||
patchData,
|
||||
@@ -20,7 +20,7 @@ export default function useGoogleSheet() {
|
||||
// functions push data to store
|
||||
const patchStepData = useSheetStore((state) => state.patchStepData);
|
||||
const setRundown = useSheetStore((state) => state.setRundown);
|
||||
const setUserFields = useSheetStore((state) => state.setUserFields);
|
||||
const setCustomFields = useSheetStore((state) => state.setCustomFields);
|
||||
|
||||
/** whether the current session has been authenticated */
|
||||
const verifyAuth = async (): Promise<{ authenticated: AuthenticationStatus } | void> => {
|
||||
@@ -43,6 +43,7 @@ export default function useGoogleSheet() {
|
||||
}
|
||||
};
|
||||
|
||||
/** requests the revoking of an existing authenticated session */
|
||||
const revoke = async (): Promise<{ authenticated: AuthenticationStatus } | void> => {
|
||||
try {
|
||||
return revokeAuthentication();
|
||||
@@ -52,18 +53,18 @@ export default function useGoogleSheet() {
|
||||
};
|
||||
|
||||
/** fetches data from a worksheet by its ID */
|
||||
const importRundownPreview = async (sheetId: string, fileOptions: ExcelImportMap) => {
|
||||
const importRundownPreview = async (sheetId: string, fileOptions: ImportMap) => {
|
||||
try {
|
||||
const data = await previewRundown(sheetId, fileOptions);
|
||||
setRundown(data.rundown);
|
||||
setUserFields(data.userFields);
|
||||
setCustomFields(data.customFields);
|
||||
} catch (error) {
|
||||
patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } });
|
||||
}
|
||||
};
|
||||
|
||||
/** writes data to a worksheet by its ID */
|
||||
const exportRundown = async (sheetId: string, fileOptions: ExcelImportMap) => {
|
||||
const exportRundown = async (sheetId: string, fileOptions: ImportMap) => {
|
||||
try {
|
||||
// write data to google
|
||||
await uploadRundown(sheetId, fileOptions);
|
||||
@@ -73,14 +74,14 @@ export default function useGoogleSheet() {
|
||||
}
|
||||
};
|
||||
|
||||
/** applies rundown and userfields to current project */
|
||||
const importRundown = async (rundown: OntimeRundown, userFields: UserFields) => {
|
||||
/** applies rundown and customFields to current project */
|
||||
const importRundown = async (rundown: OntimeRundown, customFields: CustomFields) => {
|
||||
try {
|
||||
await patchData({ rundown, userFields });
|
||||
await patchData({ rundown, customFields });
|
||||
// we are unable to optimistically set the rundown since we need
|
||||
// it to be normalised
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [...RUNDOWN, ...USERFIELDS],
|
||||
queryKey: [RUNDOWN, CUSTOM_FIELDS],
|
||||
});
|
||||
} catch (error) {
|
||||
patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } });
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { AuthenticationStatus, OntimeRundown, UserFields } from 'ontime-types';
|
||||
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
|
||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { defaultImportMap, ImportMap } from 'ontime-utils';
|
||||
import { create } from 'zustand';
|
||||
|
||||
// TODO: persist excelFileOptions to localStorage
|
||||
|
||||
type SheetStore = {
|
||||
stepData: typeof initialStepData;
|
||||
patchStepData: (patch: Partial<typeof initialStepData>) => void;
|
||||
|
||||
spreadsheet: File | null;
|
||||
setSpreadsheet: (spreadsheet: File | null) => void;
|
||||
|
||||
sheetId: string | null;
|
||||
setSheetId: (sheetId: string | null) => void;
|
||||
|
||||
authenticationStatus: AuthenticationStatus;
|
||||
setAuthenticationStatus: (status: AuthenticationStatus) => void;
|
||||
|
||||
// we get this from a preview response
|
||||
rundown: OntimeRundown | null;
|
||||
setRundown: (rundown: OntimeRundown | null) => void;
|
||||
|
||||
userFields: UserFields | null;
|
||||
setUserFields: (userFields: UserFields | null) => void;
|
||||
// we get this from a preview response
|
||||
customFields: CustomFields | null;
|
||||
setCustomFields: (customFields: CustomFields | null) => void;
|
||||
|
||||
worksheetOptions: string[] | null;
|
||||
setWorksheetOptions: (worksheetOptions: string[] | null) => void;
|
||||
|
||||
excelFileOptions: ExcelImportMap;
|
||||
patchExcelFileOptions: <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => void;
|
||||
spreadsheetImportMap: ImportMap;
|
||||
patchSpreadsheetImportMap: <T extends keyof ImportMap>(field: T, value: ImportMap[T]) => void;
|
||||
|
||||
reset: () => void;
|
||||
resetPreview: () => void;
|
||||
@@ -39,12 +39,12 @@ const initialStepData = {
|
||||
|
||||
const initialState = {
|
||||
stepData: initialStepData,
|
||||
spreadsheet: null,
|
||||
sheetId: null,
|
||||
authenticationStatus: 'not_authenticated' as AuthenticationStatus,
|
||||
rundown: null,
|
||||
userFields: null,
|
||||
worksheetOptions: null,
|
||||
excelFileOptions: defaultExcelImportMap,
|
||||
customFields: null,
|
||||
spreadsheetImportMap: defaultImportMap,
|
||||
};
|
||||
|
||||
export const useSheetStore = create<SheetStore>((set, get) => ({
|
||||
@@ -55,23 +55,23 @@ export const useSheetStore = create<SheetStore>((set, get) => ({
|
||||
set({ stepData: { ...stepData, ...patch } });
|
||||
},
|
||||
|
||||
setSpreadsheet: (spreadsheet: File | null) => set({ spreadsheet }),
|
||||
|
||||
setSheetId: (sheetId: string | null) => set({ sheetId }),
|
||||
|
||||
setAuthenticationStatus: (status: AuthenticationStatus) => set({ authenticationStatus: status }),
|
||||
|
||||
setRundown: (rundown: OntimeRundown | null) => set({ rundown }),
|
||||
|
||||
setUserFields: (userFields: UserFields | null) => set({ userFields }),
|
||||
setCustomFields: (customFields: CustomFields | null) => set({ customFields }),
|
||||
|
||||
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;
|
||||
patchSpreadsheetImportMap: <T extends keyof ImportMap>(field: T, value: ImportMap[T]) => {
|
||||
const currentImportMap = get().spreadsheetImportMap;
|
||||
if (currentImportMap[field] !== value) {
|
||||
currentImportMap[field] = value;
|
||||
}
|
||||
},
|
||||
|
||||
reset: () => set(initialState),
|
||||
resetPreview: () => set({ rundown: null, userFields: null }),
|
||||
resetPreview: () => set({ rundown: null, customFields: null }),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user