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
*/
export const postPreviewSheet = async (id: string, worksheet: string) => {
const response = await axios.post(`${ontimeURL}/sheet-preview`, {
id,
worksheet,
});
export const postPreviewSheet = async (id: string, options: ExcelImportMap) => {
console.log(options);
const response = await axios.post(`${ontimeURL}/sheet/pull`, { id, options });
return response.data.data;
};
/**
* @description STEP 5
*/
export const postPushSheet = async (id: string, worksheet: string) => {
const response = await axios.post(`${ontimeURL}/sheet-push`, {
id,
worksheet,
});
export const postPushSheet = async (id: string, options: ExcelImportMap) => {
const response = await axios.post(`${ontimeURL}/sheet-push`, { id, options });
return response.data.data;
};
@@ -18,6 +18,7 @@ import {
} 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';
@@ -37,6 +38,7 @@ 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';
@@ -58,6 +60,9 @@ export default function SheetsModal(props: SheetsModalProps) {
const [worksheet, setWorksheet] = useState('');
const [worksheetOptions, setWorksheetOptions] = useState(new Array<string>());
const [direction, setDirection] = useState('none');
const excelFileOptions = useRef<ExcelImportMap>(defaultExcelImportMap);
const fileInputRef = useRef<HTMLInputElement>(null);
const [state, setState] = useState({
@@ -70,6 +75,7 @@ export default function SheetsModal(props: SheetsModalProps) {
useEffect(() => {
if (isOpen) {
setDirection('none');
testClientSecrect();
if (state.clientSecret.complet) testAuthentication();
if (state.authenticate.complet) testSheetId();
@@ -190,6 +196,7 @@ export default function SheetsModal(props: SheetsModalProps) {
//SETP-4 Select Worksheet
const testWorksheet = (value: string) => {
excelFileOptions.current.worksheet = value;
setWorksheet(value);
postWorksheet(id, worksheet)
.then(() => {
@@ -202,16 +209,32 @@ export default function SheetsModal(props: SheetsModalProps) {
};
//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, worksheet).then((data) => {
setProject(data.project);
setRundown(data.rundown);
setUserFields(data.userFields);
});
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: { complet: false, message } });
});
};
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
@@ -266,119 +289,125 @@ export default function SheetsModal(props: SheetsModalProps) {
</div>
</Alert>
{!rundown ? (
<>
<Step
title='1 - Upload OAuth 2.0 Client ID'
completed={state.clientSecret.complet}
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.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}
direction === 'up' ? (
<div>{direction}</div>
) : direction === 'down' ? (
<ExcelFileOptions optionsRef={excelFileOptions} updateOptions={updateExcelFileOptions} />
) : (
<>
<Step
title='1 - Upload OAuth 2.0 Client ID'
completed={state.clientSecret.complet}
disabled={false}
error={state.clientSecret.message}
>
{worksheetOptions.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</Select>
</Step>
<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.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}>
<div style={{ display: 'flex', gap: '1em' }}>
<Button
disabled={!state.worksheet.complet}
variant='ontime-subtle-on-light'
padding='0 2em'
onClick={handlePushData}
<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}
>
Upload
</Button>
<Button
disabled={!state.worksheet.complet}
variant='ontime-subtle-on-light'
padding='0 2em'
onClick={handlePullData}
>
Download
</Button>
</div>
</Step>
</>
{worksheetOptions.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</Select>
</Step>
<Step title='5 - Upload / Download rundown' completed={false} disabled={!state.worksheet.complet}>
<div style={{ display: 'flex', gap: '1em' }}>
<Button
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
rundown={rundown ?? []}
@@ -388,10 +417,41 @@ export default function SheetsModal(props: SheetsModalProps) {
)}
</ModalBody>
<ModalFooter>
{rundown && (
<Button variant='ontime-filled' padding='0 2em' onClick={handleFinalise}>
Import
</Button>
{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}>
Review
</Button>
</div>
) : (
<></>
)}
</ModalFooter>
</ModalContent>
@@ -554,10 +554,10 @@ export const postWorksheet = async (req, res) => {
* @description STEP-5 POST download undown to sheet
* @returns parsed result
*/
export async function previewSheet(req, res) {
export async function pullSheet(req, res) {
try {
const { id, worksheet } = req.body;
const data = await sheet.pull(id, worksheet);
const { id, options } = req.body;
const data = await sheet.pull(id, options);
res.status(200).send(data);
} catch (error) {
res.status(500).send({ message: error.toString() });
@@ -569,8 +569,8 @@ export async function previewSheet(req, res) {
*/
export async function pushSheet(req, res) {
try {
const { id, worksheet } = req.data;
await sheet.push(id, worksheet);
const { id, options } = req.body;
await sheet.push(id, options);
res.status(200).send();
} catch (error) {
res.status(500).send({ message: error.toString() });
@@ -171,3 +171,13 @@ export const validateWorksheet = [
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,
getAuthenticationUrl,
uploadSheetClientFile,
previewSheet,
pullSheet,
pushSheet,
postId,
getAuthentication,
@@ -41,6 +41,7 @@ import {
validateOscSubscription,
validateSheetid,
validateWorksheet,
validateSheetOptions,
} from '../controllers/ontimeController.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
router.post('/new', projectSanitiser, postNew);
//SETP-1
router.post('/sheet/clientsecrect', uploadFile, uploadSheetClientFile);
router.get('/sheet/clientsecrect', uploadFile, getClientSecrect);
@@ -122,7 +122,7 @@ router.post('/sheet/id', validateSheetid, postId);
router.post('/sheet/worksheet', validateWorksheet, postId);
//STEP-5 download and generate preview
router.post('/sheet-preview', validateWorksheet, previewSheet);
router.post('/sheet/pull', validateSheetOptions, pullSheet);
//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 { parseExcel } from './parser.js';
import { parseProject, parseRundown, parseUserFields } from './parserFunctions.js';
import { ExcelImportMap } from 'ontime-utils';
type ResponseOK = {
data: Partial<DatabaseModel>;
@@ -265,11 +266,11 @@ class 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} worksheet - the name of the worksheet containing ontime data
* @param {ExcelImportMap} options
* @throws
*/
public async push(id: string, worksheet: string) {
const { worksheetId, range } = await this.exist(id, worksheet);
public async push(id: string, options: ExcelImportMap) {
const { worksheetId, range } = await this.exist(id, options.worksheet);
const rq = await sheets({ version: 'v4', auth: Sheet.client }).spreadsheets.values.get({
spreadsheetId: id,
@@ -278,7 +279,7 @@ class Sheet {
range: range,
});
if (rq.status === 200) {
const { rundownMetadata, projectMetadata } = parseExcel(rq.data.values);
const { rundownMetadata, projectMetadata } = parseExcel(rq.data.values, options);
const rundown = DataProvider.getRundown();
const projectData = DataProvider.getProjectData();
const titleRow = Object.values(rundownMetadata)[0]['row'];
@@ -344,12 +345,12 @@ class 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} worksheet - the name of the worksheet containing ontime data
* @param {ExcelImportMap} options
* @returns {Promise<Partial<ResponseOK>>}
* @throws
*/
public async pull(id: string, worksheet: string): Promise<Partial<ResponseOK>> {
const { range } = await this.exist(id, worksheet);
public async pull(id: string, options: ExcelImportMap): Promise<Partial<ResponseOK>> {
const { range } = await this.exist(id, options.worksheet);
const res: Partial<ResponseOK> = {};
@@ -362,12 +363,13 @@ class Sheet {
if (rq.status === 200) {
res.data = {};
const dataFromSheet = parseExcel(rq.data.values);
const dataFromSheet = parseExcel(rq.data.values, options);
res.data.rundown = parseRundown(dataFromSheet);
if (res.data.rundown.length < 1) {
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.userFields = parseUserFields(dataFromSheet);
return res;