feat: added export options (#529)

* feat: added export options

* chore: eslint console proposal

---------

Co-authored-by: arc-alex <ac@omnivox.dk>
This commit is contained in:
Enubia
2023-10-30 11:41:15 +01:00
committed by GitHub
parent 7c234466f7
commit 78d5d442cf
12 changed files with 218 additions and 68 deletions
+1 -1
View File
@@ -21,6 +21,6 @@
} }
], ],
"rules": { "rules": {
"no-console": "warn" "no-console": ["warn", { "allow": ["warn", "error"]}]
} }
} }
BIN
View File
Binary file not shown.
+10 -24
View File
@@ -3,6 +3,7 @@ import { Alias, OSCSettings, OscSubscription, ProjectData, Settings, UserFields,
import { apiRepoLatest } from '../../externals'; import { apiRepoLatest } from '../../externals';
import { InfoType } from '../models/Info'; import { InfoType } from '../models/Info';
import fileDownload from '../utils/fileDownload';
import { ontimeURL } from './apiConstants'; import { ontimeURL } from './apiConstants';
@@ -109,32 +110,17 @@ export async function postOscSubscriptions(data: OscSubscription) {
} }
/** /**
* @description HTTP request to download db * @description HTTP request to download db in CSV format
* @return {Promise}
*/ */
export const downloadRundown = async () => { export const downloadCSV = () => {
await axios({ return fileDownload(ontimeURL, { name: 'rundown', type: 'csv' }, { type: 'text/csv;charset=utf-8;' });
url: `${ontimeURL}/db`, };
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
const headerLine = response.headers['Content-Disposition'];
let filename = 'rundown.json';
// try and get the filename from the response /**
if (headerLine != null) { * @description HTTP request to download db in JSON format
const startFileNameIndex = headerLine.indexOf('"') + 1; */
const endFileNameIndex = headerLine.lastIndexOf('"'); export const downloadRundown = () => {
filename = headerLine.substring(startFileNameIndex, endFileNameIndex); return fileDownload(ontimeURL, { name: 'rundown', type: 'json' }, { type: 'application/json;charset=utf-8;' });
}
const url = window.URL.createObjectURL(new Blob([response.data], { type: 'application/json' }));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
});
}; };
/** /**
@@ -0,0 +1,56 @@
import axios from 'axios';
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
type FileOptions = {
name: string;
type: string;
};
type BlobOptions = {
type: string;
};
export default async function fileDownload(url: string, fileOptions: FileOptions, blobOptions: BlobOptions) {
const response = await axios({
url: `${url}/db`,
method: 'GET',
});
const headerLine = response.headers['Content-Disposition'];
let { name: fileName } = fileOptions;
const { type: fileType } = fileOptions;
const { project, rundown, userFields } = response.data;
// try and get the filename from the response
if (headerLine != null) {
const startFileNameIndex = headerLine.indexOf('"') + 1;
const endFileNameIndex = headerLine.lastIndexOf('"');
fileName = headerLine.substring(startFileNameIndex, endFileNameIndex);
}
let fileContent = '';
if (fileType === 'json') {
fileContent = JSON.stringify(response.data);
fileName += '.json';
}
if (fileType === 'csv') {
const sheetData = makeTable(project, rundown, userFields);
fileContent = makeCSV(sheetData);
fileName += '.csv';
}
const blob = new Blob([fileContent], { type: blobOptions.type });
const downloadUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.setAttribute('href', downloadUrl);
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
// Clean up the URL.createObjectURL to release resources
URL.revokeObjectURL(downloadUrl);
return;
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { OntimeRundownEntry, ProjectData } from 'ontime-types'; import { OntimeRundownEntry, ProjectData } from 'ontime-types';
import Empty from '../../common/components/state/Empty'; import Empty from '../../common/components/state/Empty';
@@ -6,6 +6,7 @@ import { useEventAction } from '../../common/hooks/useEventAction';
import { useCuesheet } from '../../common/hooks/useSocket'; import { useCuesheet } from '../../common/hooks/useSocket';
import useRundown from '../../common/hooks-query/useRundown'; import useRundown from '../../common/hooks-query/useRundown';
import useUserFields from '../../common/hooks-query/useUserFields'; import useUserFields from '../../common/hooks-query/useUserFields';
import ExportModal, { ExportType } from '../modals/export-modal/ExportModal';
import CuesheetTableHeader from './cuesheet-table-header/CuesheetTableHeader'; import CuesheetTableHeader from './cuesheet-table-header/CuesheetTableHeader';
import Cuesheet from './Cuesheet'; import Cuesheet from './Cuesheet';
@@ -20,6 +21,8 @@ export default function CuesheetWrapper() {
const { updateEvent } = useEventAction(); const { updateEvent } = useEventAction();
const featureData = useCuesheet(); const featureData = useCuesheet();
const columns = useMemo(() => makeCuesheetColumns(userFields), [userFields]); const columns = useMemo(() => makeCuesheetColumns(userFields), [userFields]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [headerData, setheaderData] = useState<ProjectData | null>(null);
// Set window title // Set window title
useEffect(() => { useEffect(() => {
@@ -69,37 +72,80 @@ export default function CuesheetWrapper() {
); );
const exportHandler = useCallback( const exportHandler = useCallback(
(headerData: ProjectData) => { (headerData: ProjectData, exportType: ExportType) => {
if (!headerData || !rundown || !userFields) { if (!headerData || !rundown || !userFields) {
return; return;
} }
const sheetData = makeTable(headerData, rundown, userFields); let fileName = '';
const csvContent = makeCSV(sheetData); let url = '';
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); if (exportType === 'json') {
const url = URL.createObjectURL(blob); const jsonContent = JSON.stringify({
headerData,
rundown,
userFields,
});
fileName = 'ontime export.json';
const blob = new Blob([jsonContent], { type: 'application/json;charset=utf-8;' });
url = URL.createObjectURL(blob);
} else if (exportType === 'csv') {
const sheetData = makeTable(headerData, rundown, userFields);
const csvContent = makeCSV(sheetData);
fileName = 'ontime export.csv';
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
url = URL.createObjectURL(blob);
} else {
console.error('Invalid export type: ', exportType);
return;
}
const link = document.createElement('a'); const link = document.createElement('a');
link.setAttribute('href', url); link.setAttribute('href', url);
link.setAttribute('download', 'ontime export.csv'); link.setAttribute('download', fileName);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
// Clean up the URL.createObjectURL to release resources // Clean up the URL.createObjectURL to release resources
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
return;
}, },
[rundown, userFields], [rundown, userFields],
); );
const onModalClose = (exportType?: ExportType) => {
setIsModalOpen(false);
if (!exportType) {
return;
}
if (headerData) {
exportHandler(headerData, exportType);
}
};
const handleOpenModal = (projectData: ProjectData) => {
setheaderData(projectData);
setIsModalOpen(true);
};
if (!rundown || !userFields) { if (!rundown || !userFields) {
return <Empty text='Loading...' />; return <Empty text='Loading...' />;
} }
return ( return (
<div className={styles.tableWrapper} data-testid='cuesheet'> <div className={styles.tableWrapper} data-testid='cuesheet'>
<CuesheetTableHeader handleCSVExport={exportHandler} featureData={featureData} /> <CuesheetTableHeader handleExport={handleOpenModal} featureData={featureData} />
<Cuesheet data={rundown} columns={columns} handleUpdate={handleUpdate} selectedId={featureData.selectedEventId} /> <Cuesheet data={rundown} columns={columns} handleUpdate={handleUpdate} selectedId={featureData.selectedEventId} />
<ExportModal
isOpen={isModalOpen}
onClose={onModalClose}
buttonVariants={{ csv: 'ontime-filled', json: 'ontime-ghosted' }}
/>
</div> </div>
); );
} }
@@ -16,7 +16,7 @@ import CuesheetTableHeaderTimers from './CuesheetTableHeaderTimers';
import style from './CuesheetTableHeader.module.scss'; import style from './CuesheetTableHeader.module.scss';
interface CuesheetTableHeaderProps { interface CuesheetTableHeaderProps {
handleCSVExport: (headerData: ProjectData) => void; handleExport: (headerData: ProjectData) => void;
featureData: { featureData: {
playback: Playback; playback: Playback;
selectedEventIndex: number | null; selectedEventIndex: number | null;
@@ -25,7 +25,7 @@ interface CuesheetTableHeaderProps {
}; };
} }
export default function CuesheetTableHeader({ handleCSVExport, featureData }: CuesheetTableHeaderProps) { export default function CuesheetTableHeader({ handleExport, featureData }: CuesheetTableHeaderProps) {
const followSelected = useCuesheetSettings((state) => state.followSelected); const followSelected = useCuesheetSettings((state) => state.followSelected);
const showSettings = useCuesheetSettings((state) => state.showSettings); const showSettings = useCuesheetSettings((state) => state.showSettings);
const toggleSettings = useCuesheetSettings((state) => state.toggleSettings); const toggleSettings = useCuesheetSettings((state) => state.toggleSettings);
@@ -33,9 +33,9 @@ export default function CuesheetTableHeader({ handleCSVExport, featureData }: Cu
const { isFullScreen, toggleFullScreen } = useFullscreen(); const { isFullScreen, toggleFullScreen } = useFullscreen();
const { data: project } = useProjectData(); const { data: project } = useProjectData();
const exportCsv = () => { const exportProject = () => {
if (project) { if (project) {
handleCSVExport(project); handleExport(project);
} }
}; };
@@ -72,9 +72,9 @@ export default function CuesheetTableHeader({ handleCSVExport, featureData }: Cu
{isFullScreen ? <IoContract /> : <IoExpand />} {isFullScreen ? <IoContract /> : <IoExpand />}
</span> </span>
</Tooltip> </Tooltip>
<Tooltip openDelay={tooltipDelayFast} label='Export rundown to CSV'> <Tooltip openDelay={tooltipDelayFast} label='Export rundown'>
<span className={style.actionIcon} onClick={exportCsv}> <span className={style.actionIcon} onClick={exportProject}>
CSV Export
</span> </span>
</Tooltip> </Tooltip>
</div> </div>
+26 -3
View File
@@ -1,4 +1,4 @@
import { memo, useCallback, useEffect } from 'react'; import { memo, useCallback, useEffect, useState } from 'react';
import { VStack } from '@chakra-ui/react'; import { VStack } from '@chakra-ui/react';
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand'; import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle'; import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
@@ -10,11 +10,12 @@ import { IoPushOutline } from '@react-icons/all-files/io5/IoPushOutline';
import { IoSaveOutline } from '@react-icons/all-files/io5/IoSaveOutline'; import { IoSaveOutline } from '@react-icons/all-files/io5/IoSaveOutline';
import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline'; import { IoSettingsOutline } from '@react-icons/all-files/io5/IoSettingsOutline';
import { downloadRundown } from '../../common/api/ontimeApi'; import { downloadCSV, downloadRundown } from '../../common/api/ontimeApi';
import QuitIconBtn from '../../common/components/buttons/QuitIconBtn'; import QuitIconBtn from '../../common/components/buttons/QuitIconBtn';
import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn'; import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import useElectronEvent from '../../common/hooks/useElectronEvent'; import useElectronEvent from '../../common/hooks/useElectronEvent';
import { AppMode, useAppMode } from '../../common/stores/appModeStore'; import { AppMode, useAppMode } from '../../common/stores/appModeStore';
import ExportModal, { ExportType } from '../modals/export-modal/ExportModal';
import style from './MenuBar.module.scss'; import style from './MenuBar.module.scss';
@@ -100,6 +101,22 @@ const MenuBar = (props: MenuBarProps) => {
}; };
}, [handleKeyPress, isElectron]); }, [handleKeyPress, isElectron]);
const [isModalOpen, setIsModalOpen] = useState(false);
const onModalClose = (exportType?: ExportType) => {
setIsModalOpen(false);
if (!exportType) {
return;
}
if (exportType === 'json') {
downloadRundown();
} else if (exportType === 'csv') {
downloadCSV();
}
};
return ( return (
<VStack> <VStack>
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} size='md' /> <QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} size='md' />
@@ -128,12 +145,18 @@ const MenuBar = (props: MenuBarProps) => {
{...buttonStyle} {...buttonStyle}
icon={<IoSaveOutline />} icon={<IoSaveOutline />}
isDisabled={appMode === AppMode.Run} isDisabled={appMode === AppMode.Run}
clickHandler={downloadRundown} clickHandler={() => setIsModalOpen(true)}
tooltip='Export project file' tooltip='Export project file'
aria-label='Export project file' aria-label='Export project file'
size='sm' size='sm'
/> />
<ExportModal
onClose={onModalClose}
isOpen={isModalOpen}
buttonVariants={{ csv: 'ontime-subtle-on-light', json: 'ontime-filled' }}
/>
<div className={style.gap} /> <div className={style.gap} />
<TooltipActionBtn <TooltipActionBtn
{...buttonStyle} {...buttonStyle}
@@ -0,0 +1,10 @@
.modalHeader {
font-weight: 400;
}
.modalBody {
display: flex;
justify-content: space-between;
margin-top: 1rem;
margin-bottom: 1rem;
}
@@ -0,0 +1,35 @@
import { Button, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
import styles from './ExportModal.module.scss';
export type ExportType = 'csv' | 'json';
interface ExportModalProps {
isOpen: boolean;
onClose: (type?: ExportType) => void;
buttonVariants: {
csv: string;
json: string;
};
}
export default function ExportModal(props: ExportModalProps) {
const { isOpen, onClose, buttonVariants } = props;
return (
<Modal isOpen={isOpen} onClose={onClose} motionPreset='scale' size='xl' colorScheme='blackAlpha'>
<ModalOverlay />
<ModalContent>
<ModalHeader className={styles.modalHeader}>Download options</ModalHeader>
<ModalCloseButton />
<ModalBody className={styles.modalBody}>
<Button onClick={() => onClose('csv')} variant={buttonVariants.csv} width='48%'>
rundown as CSV
</Button>
<Button onClick={() => onClose('json')} variant={buttonVariants.json} width='48%'>
Project file
</Button>
</ModalBody>
</ModalContent>
</Modal>
);
}
+15 -22
View File
@@ -1,16 +1,20 @@
{ {
"rundown": [ "rundown": [
{ {
"title": "First test event", "type": "block",
"id": "18797"
},
{
"title": "title 1",
"subtitle": "", "subtitle": "",
"presenter": "", "presenter": "",
"note": "", "note": "",
"endAction": "none", "endAction": "none",
"timerType": "count-down", "timerType": "count-down",
"timeStart": 32400000, "timeStart": 60000,
"timeEnd": 36000000, "timeEnd": 120000,
"duration": 3600000, "duration": 60000,
"isPublic": true, "isPublic": false,
"skip": false, "skip": false,
"colour": "", "colour": "",
"user0": "", "user0": "",
@@ -29,22 +33,16 @@
"cue": "1" "cue": "1"
}, },
{ {
"duration": 600000, "title": "title 2",
"type": "delay",
"revision": 0,
"id": "b1d5a"
},
{
"title": "Second test event",
"subtitle": "", "subtitle": "",
"presenter": "", "presenter": "",
"note": "", "note": "",
"endAction": "none", "endAction": "none",
"timerType": "count-down", "timerType": "count-down",
"timeStart": 36000000, "timeStart": 120000,
"timeEnd": 39600000, "timeEnd": 180000,
"duration": 3600000, "duration": 60000,
"isPublic": true, "isPublic": false,
"skip": false, "skip": false,
"colour": "", "colour": "",
"user0": "", "user0": "",
@@ -63,12 +61,7 @@
"cue": "2" "cue": "2"
}, },
{ {
"title": "Lunch", "title": "title 3",
"type": "block",
"id": "91682"
},
{
"title": "Third test event",
"subtitle": "", "subtitle": "",
"presenter": "", "presenter": "",
"note": "", "note": "",
+2 -1
View File
@@ -19,7 +19,8 @@ test('cuesheet displays events and exports csv', async ({ page }) => {
await page.getByRole('cell', { name: '+10 min' }).click(); await page.getByRole('cell', { name: '+10 min' }).click();
await page.getByRole('cell', { name: 'Lunch' }).click(); await page.getByRole('cell', { name: 'Lunch' }).click();
const downloadPromise = page.waitForEvent('download'); const downloadPromise = page.waitForEvent('download');
await page.getByTestId('cuesheet').getByText('CSV').click(); await page.getByTestId('cuesheet').getByText('Export').click();
await page.getByText('CSV').click();
// From here we test the CSV download feature // From here we test the CSV download feature