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": {
"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 { InfoType } from '../models/Info';
import fileDownload from '../utils/fileDownload';
import { ontimeURL } from './apiConstants';
@@ -109,32 +110,17 @@ export async function postOscSubscriptions(data: OscSubscription) {
}
/**
* @description HTTP request to download db
* @return {Promise}
* @description HTTP request to download db in CSV format
*/
export const downloadRundown = async () => {
await axios({
url: `${ontimeURL}/db`,
method: 'GET',
responseType: 'blob', // important
}).then((response) => {
const headerLine = response.headers['Content-Disposition'];
let filename = 'rundown.json';
export const downloadCSV = () => {
return fileDownload(ontimeURL, { name: 'rundown', type: 'csv' }, { type: 'text/csv;charset=utf-8;' });
};
// 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);
}
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();
});
/**
* @description HTTP request to download db in JSON format
*/
export const downloadRundown = () => {
return fileDownload(ontimeURL, { name: 'rundown', type: 'json' }, { type: 'application/json;charset=utf-8;' });
};
/**
@@ -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;
}
@@ -21,4 +21,4 @@
border: 1px solid $white-10;
border-radius: 4px;
}
}
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { OntimeRundownEntry, ProjectData } from 'ontime-types';
import Empty from '../../common/components/state/Empty';
@@ -6,6 +6,7 @@ import { useEventAction } from '../../common/hooks/useEventAction';
import { useCuesheet } from '../../common/hooks/useSocket';
import useRundown from '../../common/hooks-query/useRundown';
import useUserFields from '../../common/hooks-query/useUserFields';
import ExportModal, { ExportType } from '../modals/export-modal/ExportModal';
import CuesheetTableHeader from './cuesheet-table-header/CuesheetTableHeader';
import Cuesheet from './Cuesheet';
@@ -20,6 +21,8 @@ export default function CuesheetWrapper() {
const { updateEvent } = useEventAction();
const featureData = useCuesheet();
const columns = useMemo(() => makeCuesheetColumns(userFields), [userFields]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [headerData, setheaderData] = useState<ProjectData | null>(null);
// Set window title
useEffect(() => {
@@ -69,37 +72,80 @@ export default function CuesheetWrapper() {
);
const exportHandler = useCallback(
(headerData: ProjectData) => {
(headerData: ProjectData, exportType: ExportType) => {
if (!headerData || !rundown || !userFields) {
return;
}
const sheetData = makeTable(headerData, rundown, userFields);
const csvContent = makeCSV(sheetData);
let fileName = '';
let url = '';
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
if (exportType === 'json') {
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');
link.setAttribute('href', url);
link.setAttribute('download', 'ontime export.csv');
link.setAttribute('download', fileName);
document.body.appendChild(link);
link.click();
// Clean up the URL.createObjectURL to release resources
URL.revokeObjectURL(url);
return;
},
[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) {
return <Empty text='Loading...' />;
}
return (
<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} />
<ExportModal
isOpen={isModalOpen}
onClose={onModalClose}
buttonVariants={{ csv: 'ontime-filled', json: 'ontime-ghosted' }}
/>
</div>
);
}
@@ -16,7 +16,7 @@ import CuesheetTableHeaderTimers from './CuesheetTableHeaderTimers';
import style from './CuesheetTableHeader.module.scss';
interface CuesheetTableHeaderProps {
handleCSVExport: (headerData: ProjectData) => void;
handleExport: (headerData: ProjectData) => void;
featureData: {
playback: Playback;
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 showSettings = useCuesheetSettings((state) => state.showSettings);
const toggleSettings = useCuesheetSettings((state) => state.toggleSettings);
@@ -33,9 +33,9 @@ export default function CuesheetTableHeader({ handleCSVExport, featureData }: Cu
const { isFullScreen, toggleFullScreen } = useFullscreen();
const { data: project } = useProjectData();
const exportCsv = () => {
const exportProject = () => {
if (project) {
handleCSVExport(project);
handleExport(project);
}
};
@@ -72,9 +72,9 @@ export default function CuesheetTableHeader({ handleCSVExport, featureData }: Cu
{isFullScreen ? <IoContract /> : <IoExpand />}
</span>
</Tooltip>
<Tooltip openDelay={tooltipDelayFast} label='Export rundown to CSV'>
<span className={style.actionIcon} onClick={exportCsv}>
CSV
<Tooltip openDelay={tooltipDelayFast} label='Export rundown'>
<span className={style.actionIcon} onClick={exportProject}>
Export
</span>
</Tooltip>
</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 { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
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 { 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 TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn';
import useElectronEvent from '../../common/hooks/useElectronEvent';
import { AppMode, useAppMode } from '../../common/stores/appModeStore';
import ExportModal, { ExportType } from '../modals/export-modal/ExportModal';
import style from './MenuBar.module.scss';
@@ -100,6 +101,22 @@ const MenuBar = (props: MenuBarProps) => {
};
}, [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 (
<VStack>
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} size='md' />
@@ -128,12 +145,18 @@ const MenuBar = (props: MenuBarProps) => {
{...buttonStyle}
icon={<IoSaveOutline />}
isDisabled={appMode === AppMode.Run}
clickHandler={downloadRundown}
clickHandler={() => setIsModalOpen(true)}
tooltip='Export project file'
aria-label='Export project file'
size='sm'
/>
<ExportModal
onClose={onModalClose}
isOpen={isModalOpen}
buttonVariants={{ csv: 'ontime-subtle-on-light', json: 'ontime-filled' }}
/>
<div className={style.gap} />
<TooltipActionBtn
{...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": [
{
"title": "First test event",
"type": "block",
"id": "18797"
},
{
"title": "title 1",
"subtitle": "",
"presenter": "",
"note": "",
"endAction": "none",
"timerType": "count-down",
"timeStart": 32400000,
"timeEnd": 36000000,
"duration": 3600000,
"isPublic": true,
"timeStart": 60000,
"timeEnd": 120000,
"duration": 60000,
"isPublic": false,
"skip": false,
"colour": "",
"user0": "",
@@ -29,22 +33,16 @@
"cue": "1"
},
{
"duration": 600000,
"type": "delay",
"revision": 0,
"id": "b1d5a"
},
{
"title": "Second test event",
"title": "title 2",
"subtitle": "",
"presenter": "",
"note": "",
"endAction": "none",
"timerType": "count-down",
"timeStart": 36000000,
"timeEnd": 39600000,
"duration": 3600000,
"isPublic": true,
"timeStart": 120000,
"timeEnd": 180000,
"duration": 60000,
"isPublic": false,
"skip": false,
"colour": "",
"user0": "",
@@ -63,12 +61,7 @@
"cue": "2"
},
{
"title": "Lunch",
"type": "block",
"id": "91682"
},
{
"title": "Third test event",
"title": "title 3",
"subtitle": "",
"presenter": "",
"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: 'Lunch' }).click();
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