add ExcelImport

This commit is contained in:
arc-alex
2024-01-06 18:21:22 +01:00
parent 341d554bc5
commit 08a32a7265
6 changed files with 215 additions and 148 deletions
+5 -10
View File
@@ -306,21 +306,16 @@ export const postWorksheet = async (id: string, worksheet: string) => {
/** /**
* @description STEP 5 * @description STEP 5
*/ */
export const postPreviewSheet = async (id: string, worksheet: string) => { export const postPreviewSheet = async (id: string, options: ExcelImportMap) => {
const response = await axios.post(`${ontimeURL}/sheet-preview`, { console.log(options);
id, const response = await axios.post(`${ontimeURL}/sheet/pull`, { id, options });
worksheet,
});
return response.data.data; return response.data.data;
}; };
/** /**
* @description STEP 5 * @description STEP 5
*/ */
export const postPushSheet = async (id: string, worksheet: string) => { export const postPushSheet = async (id: string, options: ExcelImportMap) => {
const response = await axios.post(`${ontimeURL}/sheet-push`, { const response = await axios.post(`${ontimeURL}/sheet-push`, { id, options });
id,
worksheet,
});
return response.data.data; return response.data.data;
}; };
@@ -18,6 +18,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { OntimeRundown, ProjectData, UserFields } from 'ontime-types'; import { OntimeRundown, ProjectData, UserFields } from 'ontime-types';
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
import { PROJECT_DATA, RUNDOWN, USERFIELDS } from '../../../common/api/apiConstants'; import { PROJECT_DATA, RUNDOWN, USERFIELDS } from '../../../common/api/apiConstants';
import { maybeAxiosError } from '../../../common/api/apiUtils'; import { maybeAxiosError } from '../../../common/api/apiUtils';
@@ -37,6 +38,7 @@ import { userFieldsPlaceholder } from '../../../common/models/UserFields';
import { openLink } from '../../../common/utils/linkUtils'; import { openLink } from '../../../common/utils/linkUtils';
import ModalLink from '../ModalLink'; import ModalLink from '../ModalLink';
import PreviewExcel from '../upload-modal/preview/PreviewExcel'; import PreviewExcel from '../upload-modal/preview/PreviewExcel';
import ExcelFileOptions from '../upload-modal/upload-options/ExcelFileOptions';
import Step from './Step'; import Step from './Step';
@@ -58,6 +60,9 @@ export default function SheetsModal(props: SheetsModalProps) {
const [worksheet, setWorksheet] = useState(''); const [worksheet, setWorksheet] = useState('');
const [worksheetOptions, setWorksheetOptions] = useState(new Array<string>()); const [worksheetOptions, setWorksheetOptions] = useState(new Array<string>());
const [direction, setDirection] = useState('none');
const excelFileOptions = useRef<ExcelImportMap>(defaultExcelImportMap);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [state, setState] = useState({ const [state, setState] = useState({
@@ -70,6 +75,7 @@ export default function SheetsModal(props: SheetsModalProps) {
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
setDirection('none');
testClientSecrect(); testClientSecrect();
if (state.clientSecret.complet) testAuthentication(); if (state.clientSecret.complet) testAuthentication();
if (state.authenticate.complet) testSheetId(); if (state.authenticate.complet) testSheetId();
@@ -190,6 +196,7 @@ export default function SheetsModal(props: SheetsModalProps) {
//SETP-4 Select Worksheet //SETP-4 Select Worksheet
const testWorksheet = (value: string) => { const testWorksheet = (value: string) => {
excelFileOptions.current.worksheet = value;
setWorksheet(value); setWorksheet(value);
postWorksheet(id, worksheet) postWorksheet(id, worksheet)
.then(() => { .then(() => {
@@ -202,16 +209,32 @@ export default function SheetsModal(props: SheetsModalProps) {
}; };
//SETP-5 Upload / Download //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 = () => { const handlePullData = () => {
postPreviewSheet(id, worksheet).then((data) => { postPreviewSheet(id, excelFileOptions.current)
setProject(data.project); .then((data) => {
setRundown(data.rundown); setProject(data.project);
setUserFields(data.userFields); setRundown(data.rundown);
}); setUserFields(data.userFields);
})
.catch((error) => {
const message = maybeAxiosError(error);
setDirection('none');
setState({ ...state, pullPush: { complet: false, message } });
});
}; };
const handlePushData = () => { const handlePushData = () => {
postPushSheet(id, worksheet); postPushSheet(id, excelFileOptions.current).catch((error) => {
const message = maybeAxiosError(error);
setDirection('none');
setState({ ...state, pullPush: { complet: false, message } });
});
}; };
//GET preview //GET preview
@@ -266,119 +289,125 @@ export default function SheetsModal(props: SheetsModalProps) {
</div> </div>
</Alert> </Alert>
{!rundown ? ( {!rundown ? (
<> direction === 'up' ? (
<Step <div>{direction}</div>
title='1 - Upload OAuth 2.0 Client ID' ) : direction === 'down' ? (
completed={state.clientSecret.complet} <ExcelFileOptions optionsRef={excelFileOptions} updateOptions={updateExcelFileOptions} />
disabled={false} ) : (
error={state.clientSecret.message} <>
> <Step
<Input title='1 - Upload OAuth 2.0 Client ID'
ref={fileInputRef} completed={state.clientSecret.complet}
style={{ display: 'none' }} disabled={false}
type='file' error={state.clientSecret.message}
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.complet ? 'Reupload Client ID' : 'Upload Client ID'}
</Button>
<Button size='sm' variant='ontime-ghosted-on-light' onClick={testClientSecrect}>
Retry Client ID
</Button>
</div>
</Step>
<Step
title='2 - Authenticate with Google'
completed={state.authenticate.complet}
disabled={!state.clientSecret.complet}
error={state.authenticate.message}
>
<div style={{ display: 'flex', gap: '1em' }}>
<Button
size='sm'
variant='ontime-subtle-on-light'
onClick={handleAuthenticate}
isDisabled={!state.clientSecret.complet}
>
Authenticate
</Button>
<Button
size='sm'
variant='ontime-ghosted-on-light'
onClick={testAuthentication}
isDisabled={!state.clientSecret.complet}
>
Retry Connection
</Button>
</div>
</Step>
<Step
title='3 - Add Document ID'
completed={state.id.complet}
disabled={!state.authenticate.complet}
error={state.id.message}
>
<HStack>
<Input
type='text'
size='sm'
variant='ontime-filled-on-light'
disabled={!state.authenticate.complet}
value={id}
onChange={(event) => setSheetId(event.target.value)}
/>
<Button 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.complet}
disabled={worksheetOptions.length == 0}
>
<Select
size='sm'
disabled={worksheetOptions.length == 0}
placeholder='Select a worksheet'
onChange={(event) => testWorksheet(event.target.value)}
value={worksheet}
> >
{worksheetOptions.map((value) => ( <Input
<option key={value} value={value}> ref={fileInputRef}
{value} style={{ display: 'none' }}
</option> type='file'
))} onChange={handleFile}
</Select> accept='.json'
</Step> data-testid='file-input'
/>
<div style={{ display: 'flex', gap: '1em' }}>
<Button size='sm' variant='ontime-subtle-on-light' onClick={handleClick}>
{state.clientSecret.complet ? 'Reupload Client ID' : 'Upload Client ID'}
</Button>
<Button size='sm' variant='ontime-ghosted-on-light' onClick={testClientSecrect}>
Retry Client ID
</Button>
</div>
</Step>
<Step title='5 - Upload / Download rundown' completed={false} disabled={!state.worksheet.complet}> <Step
<div style={{ display: 'flex', gap: '1em' }}> title='2 - Authenticate with Google'
<Button completed={state.authenticate.complet}
disabled={!state.worksheet.complet} disabled={!state.clientSecret.complet}
variant='ontime-subtle-on-light' error={state.authenticate.message}
padding='0 2em' >
onClick={handlePushData} <div style={{ display: 'flex', gap: '1em' }}>
<Button
size='sm'
variant='ontime-subtle-on-light'
onClick={handleAuthenticate}
isDisabled={!state.clientSecret.complet}
>
Authenticate
</Button>
<Button
size='sm'
variant='ontime-ghosted-on-light'
onClick={testAuthentication}
isDisabled={!state.clientSecret.complet}
>
Retry Connection
</Button>
</div>
</Step>
<Step
title='3 - Add Document ID'
completed={state.id.complet}
disabled={!state.authenticate.complet}
error={state.id.message}
>
<HStack>
<Input
type='text'
size='sm'
variant='ontime-filled-on-light'
disabled={!state.authenticate.complet}
value={id}
onChange={(event) => setSheetId(event.target.value)}
/>
<Button 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.complet}
disabled={worksheetOptions.length == 0}
>
<Select
size='sm'
disabled={worksheetOptions.length == 0}
placeholder='Select a worksheet'
onChange={(event) => testWorksheet(event.target.value)}
value={worksheet}
> >
Upload {worksheetOptions.map((value) => (
</Button> <option key={value} value={value}>
<Button {value}
disabled={!state.worksheet.complet} </option>
variant='ontime-subtle-on-light' ))}
padding='0 2em' </Select>
onClick={handlePullData} </Step>
>
Download <Step title='5 - Upload / Download rundown' completed={false} disabled={!state.worksheet.complet}>
</Button> <div style={{ display: 'flex', gap: '1em' }}>
</div> <Button
</Step> disabled={!state.worksheet.complet}
</> variant='ontime-subtle-on-light'
padding='0 2em'
onClick={() => setDirection('up')}
>
Upload
</Button>
<Button
disabled={!state.worksheet.complet}
variant='ontime-subtle-on-light'
padding='0 2em'
onClick={() => setDirection('down')}
>
Download
</Button>
</div>
</Step>
</>
)
) : ( ) : (
<PreviewExcel <PreviewExcel
rundown={rundown ?? []} rundown={rundown ?? []}
@@ -388,10 +417,41 @@ export default function SheetsModal(props: SheetsModalProps) {
)} )}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
{rundown && ( {rundown ? (
<Button variant='ontime-filled' padding='0 2em' onClick={handleFinalise}> <div style={{ display: 'flex', gap: '1em' }}>
Import <Button
</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}>
Review
</Button>
</div>
) : (
<></>
)} )}
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -554,10 +554,10 @@ export const postWorksheet = async (req, res) => {
* @description STEP-5 POST download undown to sheet * @description STEP-5 POST download undown to sheet
* @returns parsed result * @returns parsed result
*/ */
export async function previewSheet(req, res) { export async function pullSheet(req, res) {
try { try {
const { id, worksheet } = req.body; const { id, options } = req.body;
const data = await sheet.pull(id, worksheet); const data = await sheet.pull(id, options);
res.status(200).send(data); res.status(200).send(data);
} catch (error) { } catch (error) {
res.status(500).send({ message: error.toString() }); res.status(500).send({ message: error.toString() });
@@ -569,8 +569,8 @@ export async function previewSheet(req, res) {
*/ */
export async function pushSheet(req, res) { export async function pushSheet(req, res) {
try { try {
const { id, worksheet } = req.data; const { id, options } = req.body;
await sheet.push(id, worksheet); await sheet.push(id, options);
res.status(200).send(); res.status(200).send();
} catch (error) { } catch (error) {
res.status(500).send({ message: error.toString() }); res.status(500).send({ message: error.toString() });
@@ -171,3 +171,13 @@ export const validateWorksheet = [
next(); next();
}, },
]; ];
export const validateSheetOptions = [
body('id').exists().isString(),
// body('options').exists().isObject(), TODO:
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
next();
},
];
+4 -4
View File
@@ -23,7 +23,7 @@ import {
postHTTP, postHTTP,
getAuthenticationUrl, getAuthenticationUrl,
uploadSheetClientFile, uploadSheetClientFile,
previewSheet, pullSheet,
pushSheet, pushSheet,
postId, postId,
getAuthentication, getAuthentication,
@@ -41,6 +41,7 @@ import {
validateOscSubscription, validateOscSubscription,
validateSheetid, validateSheetid,
validateWorksheet, validateWorksheet,
validateSheetOptions,
} from '../controllers/ontimeController.validate.js'; } from '../controllers/ontimeController.validate.js';
import { projectSanitiser } from '../controllers/projectController.validate.js'; import { projectSanitiser } from '../controllers/projectController.validate.js';
@@ -106,7 +107,6 @@ router.post('/http', validateHTTP, postHTTP);
// create route between controller and '/ontime/new' endpoint // create route between controller and '/ontime/new' endpoint
router.post('/new', projectSanitiser, postNew); router.post('/new', projectSanitiser, postNew);
//SETP-1 //SETP-1
router.post('/sheet/clientsecrect', uploadFile, uploadSheetClientFile); router.post('/sheet/clientsecrect', uploadFile, uploadSheetClientFile);
router.get('/sheet/clientsecrect', uploadFile, getClientSecrect); router.get('/sheet/clientsecrect', uploadFile, getClientSecrect);
@@ -122,7 +122,7 @@ router.post('/sheet/id', validateSheetid, postId);
router.post('/sheet/worksheet', validateWorksheet, postId); router.post('/sheet/worksheet', validateWorksheet, postId);
//STEP-5 download and generate preview //STEP-5 download and generate preview
router.post('/sheet-preview', validateWorksheet, previewSheet); router.post('/sheet/pull', validateSheetOptions, pullSheet);
//STEP-5 upload //STEP-5 upload
router.post('/sheet-push', validateWorksheet, pushSheet); router.post('/sheet-push', validateSheetOptions, pushSheet);
+11 -9
View File
@@ -13,6 +13,7 @@ import { ensureDirectory } from './fileManagement.js';
import { cellRequenstFromEvent, cellRequenstFromProjectData, getA1Notation } from './sheetUtils.js'; import { cellRequenstFromEvent, cellRequenstFromProjectData, getA1Notation } from './sheetUtils.js';
import { parseExcel } from './parser.js'; import { parseExcel } from './parser.js';
import { parseProject, parseRundown, parseUserFields } from './parserFunctions.js'; import { parseProject, parseRundown, parseUserFields } from './parserFunctions.js';
import { ExcelImportMap } from 'ontime-utils';
type ResponseOK = { type ResponseOK = {
data: Partial<DatabaseModel>; data: Partial<DatabaseModel>;
@@ -265,11 +266,11 @@ class Sheet {
/** /**
* @description SETP 5 - Upload the rundown to sheet * @description SETP 5 - Upload the rundown to sheet
* @param {string} id - id of the sheet https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0 * @param {string} id - id of the sheet https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0
* @param {string} worksheet - the name of the worksheet containing ontime data * @param {ExcelImportMap} options
* @throws * @throws
*/ */
public async push(id: string, worksheet: string) { public async push(id: string, options: ExcelImportMap) {
const { worksheetId, range } = await this.exist(id, worksheet); const { worksheetId, range } = await this.exist(id, options.worksheet);
const rq = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({ const rq = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
spreadsheetId: id, spreadsheetId: id,
@@ -278,7 +279,7 @@ class Sheet {
range: range, range: range,
}); });
if (rq.status === 200) { if (rq.status === 200) {
const { rundownMetadata, projectMetadata } = parseExcel(rq.data.values); const { rundownMetadata, projectMetadata } = parseExcel(rq.data.values, options);
const rundown = DataProvider.getRundown(); const rundown = DataProvider.getRundown();
const projectData = DataProvider.getProjectData(); const projectData = DataProvider.getProjectData();
const titleRow = Object.values(rundownMetadata)[0]['row']; const titleRow = Object.values(rundownMetadata)[0]['row'];
@@ -344,12 +345,12 @@ class Sheet {
/** /**
* @description SETP 5 - Downpload the rundown from sheet * @description SETP 5 - Downpload the rundown from sheet
* @param {string} id - id of the sheet https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0 * @param {string} id - id of the sheet https://docs.google.com/spreadsheets/d/[[spreadsheetId]]/edit#gid=0
* @param {string} worksheet - the name of the worksheet containing ontime data * @param {ExcelImportMap} options
* @returns {Promise<Partial<ResponseOK>>} * @returns {Promise<Partial<ResponseOK>>}
* @throws * @throws
*/ */
public async pull(id: string, worksheet: string): Promise<Partial<ResponseOK>> { public async pull(id: string, options: ExcelImportMap): Promise<Partial<ResponseOK>> {
const { range } = await this.exist(id, worksheet); const { range } = await this.exist(id, options.worksheet);
const res: Partial<ResponseOK> = {}; const res: Partial<ResponseOK> = {};
@@ -362,12 +363,13 @@ class Sheet {
if (rq.status === 200) { if (rq.status === 200) {
res.data = {}; res.data = {};
const dataFromSheet = parseExcel(rq.data.values); const dataFromSheet = parseExcel(rq.data.values, options);
res.data.rundown = parseRundown(dataFromSheet); res.data.rundown = parseRundown(dataFromSheet);
if (res.data.rundown.length < 1) { if (res.data.rundown.length < 1) {
throw new Error(`Sheet: Could not find data to import in the worksheet`); throw new Error(`Sheet: Could not find data to import in the worksheet`);
} }
console.log(parseProject(dataFromSheet)); console.log(dataFromSheet.project);
console.log(dataFromSheet.userFields);
res.data.project = parseProject(dataFromSheet); res.data.project = parseProject(dataFromSheet);
res.data.userFields = parseUserFields(dataFromSheet); res.data.userFields = parseUserFields(dataFromSheet);
return res; return res;