mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-17 21:24:11 +00:00
Merge remote-tracking branch 'origin/master' into v3
This commit is contained in:
@@ -8,6 +8,7 @@ export const PROJECT_DATA = ['project'];
|
||||
export const PROJECT_LIST = ['projectList'];
|
||||
export const RUNDOWN = ['rundown'];
|
||||
export const RUNTIME = ['runtimeStore'];
|
||||
export const SHEET_STATE = ['sheetState'];
|
||||
export const USERFIELDS = ['userFields'];
|
||||
export const VIEW_SETTINGS = ['viewSettings'];
|
||||
|
||||
|
||||
@@ -257,3 +257,76 @@ export async function getProjects(): Promise<ProjectFileListResponse> {
|
||||
const res = await axios.get(`${ontimeURL}/projects`);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description STEP 1
|
||||
*/
|
||||
export const uploadSheetClientFile = async (file: File) => {
|
||||
const formData = new FormData();
|
||||
formData.append('userFile', file);
|
||||
const res = await axios
|
||||
.post(`${ontimeURL}/sheet/clientsecret`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
.then((response) => response.data.id);
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 1 test
|
||||
*/
|
||||
export const getClientSecrect = async () => {
|
||||
const response = await axios.get(`${ontimeURL}/sheet/clientsecret`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 2
|
||||
*/
|
||||
export const getSheetsAuthUrl = async () => {
|
||||
const response = await axios.get(`${ontimeURL}/sheet/authentication/url`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 2 test
|
||||
*/
|
||||
export const getAuthentication = async () => {
|
||||
const response = await axios.get(`${ontimeURL}/sheet/authentication`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 3
|
||||
* @returns worksheetOptions
|
||||
*/
|
||||
export const postId = async (id: string) => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet/id`, { id });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 4
|
||||
*/
|
||||
export const postWorksheet = async (id: string, worksheet: string) => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet/worksheet`, { id, worksheet });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 5
|
||||
*/
|
||||
export const postPreviewSheet = async (id: string, options: ExcelImportMap) => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet/pull`, { id, options });
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description STEP 5
|
||||
*/
|
||||
export const postPushSheet = async (id: string, options: ExcelImportMap) => {
|
||||
const response = await axios.post(`${ontimeURL}/sheet-push`, { id, options });
|
||||
return response.data.data;
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SettingsOptionId, useSettingsStore } from '../app-settings/settingsStor
|
||||
import MenuBar from '../menu/MenuBar';
|
||||
import AboutModal from '../modals/about-modal/AboutModal';
|
||||
import QuickStart from '../modals/quick-start/QuickStart';
|
||||
import SheetsModal from '../modals/sheets-modal/SheetsModal';
|
||||
import UploadModal from '../modals/upload-modal/UploadModal';
|
||||
import Overview from '../overview/Overview';
|
||||
|
||||
@@ -21,22 +22,6 @@ const EventEditor = lazy(() => import('../event-editor/EventEditorExport'));
|
||||
const IntegrationModal = lazy(() => import('../modals/integration-modal/IntegrationModal'));
|
||||
const SettingsModal = lazy(() => import('../modals/settings-modal/SettingsModal'));
|
||||
|
||||
// TODO: add breakpoints for body font size ??
|
||||
// - 15px for normal
|
||||
// - 16px for large screens
|
||||
|
||||
// TODO: can we delete all the font-family stuff and leave it only at the top?
|
||||
|
||||
// TODO: change scrollbar colours to use ontime stuff?
|
||||
|
||||
// TODO: rename v2Styles to appStyles?
|
||||
|
||||
// TODO: remove onAir as a setting
|
||||
|
||||
// TODO: add error boundaries
|
||||
|
||||
// TODO: should nav menu have same rules as app settings
|
||||
|
||||
export default function Editor() {
|
||||
const showSettings = useSettingsStore((state) => state.showSettings);
|
||||
const setShowSettings = useSettingsStore((state) => state.setShowSettings);
|
||||
@@ -54,6 +39,7 @@ export default function Editor() {
|
||||
} = useDisclosure();
|
||||
const { isOpen: isAboutModalOpen, onOpen: onAboutModalOpen, onClose: onAboutModalClose } = useDisclosure();
|
||||
const { isOpen: isQuickStartOpen, onOpen: onQuickStartOpen, onClose: onQuickStartClose } = useDisclosure();
|
||||
const { isOpen: isSheetsOpen, onOpen: onSheetsOpen, onClose: onSheetsClose } = useDisclosure();
|
||||
|
||||
// Set window title
|
||||
useEffect(() => {
|
||||
@@ -70,8 +56,9 @@ export default function Editor() {
|
||||
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
|
||||
<AboutModal onClose={onAboutModalClose} isOpen={isAboutModalOpen} />
|
||||
<SettingsModal isOpen={isOldSettingsOpen} onClose={onSettingsClose} />
|
||||
<SheetsModal onClose={onSheetsClose} isOpen={isSheetsOpen} />
|
||||
</ErrorBoundary>
|
||||
<div className={styles.mainContainer} data-testid='editor-container'>
|
||||
<div className={styles.mainContainer} data-testid='event-editor'>
|
||||
<ErrorBoundary>
|
||||
<MenuBar
|
||||
isOldSettingsOpen={isOldSettingsOpen}
|
||||
@@ -87,6 +74,8 @@ export default function Editor() {
|
||||
onQuickStartOpen={onQuickStartOpen}
|
||||
openSettings={handleSettings}
|
||||
isSettingsOpen={isSettingsOpen}
|
||||
isSheetsOpen={isSheetsOpen}
|
||||
onSheetsOpen={onSheetsOpen}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
{showSettings ? (
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { IoCloud } from '@react-icons/all-files/io5/IoCloud';
|
||||
import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
|
||||
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
|
||||
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
|
||||
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
|
||||
@@ -30,6 +32,8 @@ interface MenuBarProps {
|
||||
onAboutOpen: () => void;
|
||||
isQuickStartOpen: boolean;
|
||||
onQuickStartOpen: () => void;
|
||||
isSheetsOpen: boolean;
|
||||
onSheetsOpen: () => void;
|
||||
openSettings: (newTab?: string) => void;
|
||||
isSettingsOpen: boolean;
|
||||
}
|
||||
@@ -61,6 +65,8 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
onQuickStartOpen,
|
||||
openSettings,
|
||||
isSettingsOpen,
|
||||
isSheetsOpen,
|
||||
onSheetsOpen,
|
||||
} = props;
|
||||
const { isElectron, sendToElectron } = useElectronEvent();
|
||||
|
||||
@@ -177,6 +183,16 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
/>
|
||||
|
||||
<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'
|
||||
size='sm'
|
||||
/>
|
||||
<TooltipActionBtn
|
||||
{...buttonStyle}
|
||||
isDisabled={appMode === AppMode.Run}
|
||||
|
||||
@@ -0,0 +1,469 @@
|
||||
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, ProjectData, UserFields } from 'ontime-types';
|
||||
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
|
||||
|
||||
import { PROJECT_DATA, 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 { projectDataPlaceholder } from '../../../common/models/ProjectData';
|
||||
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 [project, setProject] = useState<ProjectData | 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);
|
||||
setProject(null);
|
||||
setUserFields(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
//SETP-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: '' },
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//SETP-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: '' },
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//SETP-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([]);
|
||||
});
|
||||
};
|
||||
|
||||
//SETP-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: '' } });
|
||||
});
|
||||
};
|
||||
|
||||
//SETP-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) => {
|
||||
setProject(data.project);
|
||||
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 && project) {
|
||||
let doClose = false;
|
||||
try {
|
||||
await patchData({ rundown, userFields, project });
|
||||
queryClient.setQueryData(RUNDOWN, rundown);
|
||||
queryClient.setQueryData(USERFIELDS, userFields);
|
||||
queryClient.setQueryData(PROJECT_DATA, project);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [...RUNDOWN, ...USERFIELDS, ...PROJECT_DATA],
|
||||
});
|
||||
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 ?? []}
|
||||
project={project ?? projectDataPlaceholder}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
@use '../../../theme/ontimeColours' as *;
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
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,7 @@ $skip-opacity: 0.1;
|
||||
gap: 0.5rem;
|
||||
|
||||
.indicator {
|
||||
background-color: $gray-1250;
|
||||
background-color: transparent;
|
||||
margin: 0.4rem;
|
||||
margin-right: 0;
|
||||
border-radius: 0.7rem;
|
||||
@@ -217,15 +217,11 @@ $skip-opacity: 0.1;
|
||||
.indicator.delay {
|
||||
background-color: $ontime-delay;
|
||||
}
|
||||
.indicator.overlap {
|
||||
background-color: $gray-600;
|
||||
}
|
||||
.indicator.nextDay,
|
||||
.indicator.overlap,
|
||||
.indicator.spacing {
|
||||
background-color: $gray-600;
|
||||
}
|
||||
.indicator.nextDay {
|
||||
background-color: $gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
.eventStatus {
|
||||
|
||||
@@ -137,29 +137,28 @@ const EventBlockInner = (props: EventBlockInnerProps) => {
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className={style.indicators}>
|
||||
<Tooltip
|
||||
label={
|
||||
delayTime && (
|
||||
{delayTime && (
|
||||
<Tooltip
|
||||
label={
|
||||
<div>
|
||||
{delayTime}
|
||||
<br />
|
||||
{delayTime} <br />
|
||||
New Time: {newTime}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className={`${style.indicator} ${delayTime ? style.delay : ''}`} />
|
||||
</Tooltip>
|
||||
<Tooltip label={overlapTime}>
|
||||
<div
|
||||
className={`${style.indicator} ${
|
||||
overlap > 0 ? style.overlap : overlap < 0 && overlapTime !== null ? style.spacing : ''
|
||||
}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label='Start time is later than end'>
|
||||
<div className={`${style.indicator} ${timeStart > timeEnd ? style.nextDay : ''}`} />
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<div className={`${style.indicator} ${style.delay}`} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{overlapTime && (
|
||||
<Tooltip label={overlapTime}>
|
||||
<div className={`${style.indicator} ${overlap > 0 ? style.overlap : style.spacing}`} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{timeStart > timeEnd && (
|
||||
<Tooltip label='Start time is later than end'>
|
||||
<div className={`${style.indicator} ${style.nextDay}`} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<EventBlockPlayback
|
||||
|
||||
Reference in New Issue
Block a user