mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 15:09:10 +00:00
refactor: remove userFields (#791)
* refactor: process custom fields on cache generate * refactor: remove userFields
This commit is contained in:
@@ -9,7 +9,6 @@ export const PROJECT_LIST = ['projectList'];
|
|||||||
export const RUNDOWN = ['rundown'];
|
export const RUNDOWN = ['rundown'];
|
||||||
export const RUNTIME = ['runtimeStore'];
|
export const RUNTIME = ['runtimeStore'];
|
||||||
export const SHEET_STATE = ['sheetState'];
|
export const SHEET_STATE = ['sheetState'];
|
||||||
export const USERFIELDS = ['userFields'];
|
|
||||||
export const CUSTOM_FIELDS = ['customFields'];
|
export const CUSTOM_FIELDS = ['customFields'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ import {
|
|||||||
ProjectData,
|
ProjectData,
|
||||||
ProjectFileListResponse,
|
ProjectFileListResponse,
|
||||||
Settings,
|
Settings,
|
||||||
UserFields,
|
|
||||||
ViewSettings,
|
ViewSettings,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { ExcelImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiRepoLatest } from '../../externals';
|
import { apiRepoLatest } from '../../externals';
|
||||||
import fileDownload from '../utils/fileDownload';
|
import fileDownload from '../utils/fileDownload';
|
||||||
@@ -84,23 +83,6 @@ export async function postAliases(data: Alias[]) {
|
|||||||
return axios.post(`${ontimeURL}/aliases`, data);
|
return axios.post(`${ontimeURL}/aliases`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to retrieve user fields
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function getUserFields(): Promise<UserFields> {
|
|
||||||
const res = await axios.get(`${ontimeURL}/userfields`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to mutate user fields
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function postUserFields(data: UserFields) {
|
|
||||||
return axios.post(`${ontimeURL}/userfields`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to retrieve osc settings
|
* @description HTTP request to retrieve osc settings
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
@@ -189,40 +171,35 @@ export const uploadProjectFile = async (
|
|||||||
* @description Make patch changes to the objects in the db
|
* @description Make patch changes to the objects in the db
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
export async function patchData(patchDb: Partial<DatabaseModel>) {
|
export async function patchData(patchDb: Partial<DatabaseModel>): Promise<void> {
|
||||||
const response = await axios.patch(`${ontimeURL}/db`, patchDb);
|
return await axios.patch(`${ontimeURL}/db`, patchDb);
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PostPreviewExcelResponse = {
|
type PreviewSpreadsheetResponse = {
|
||||||
rundown: OntimeRundown;
|
rundown: OntimeRundown;
|
||||||
userFields: UserFields;
|
customFields: CustomFields;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Make patch changes to the objects in the db
|
* @description Make patch changes to the objects in the db
|
||||||
* @return {Promise} - returns parsed rundown and userfields
|
* @return {Promise} - returns parsed rundown and customFields
|
||||||
*/
|
*/
|
||||||
export async function postPreviewExcel(file: File, setProgress: (value: number) => void, options?: ExcelImportMap) {
|
export async function importSpreadsheetPreview(file: File, options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('userFile', file);
|
formData.append('userFile', file);
|
||||||
formData.append('options', JSON.stringify(options));
|
formData.append('options', JSON.stringify(options));
|
||||||
|
|
||||||
const response: AxiosResponse<PostPreviewExcelResponse> = await axios.post(
|
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(
|
||||||
`${ontimeURL}/preview-spreadsheet`,
|
`${ontimeURL}/spreadsheet/preview`,
|
||||||
formData,
|
formData,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
},
|
},
|
||||||
onUploadProgress: (progressEvent) => {
|
|
||||||
const complete = progressEvent?.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) : 0;
|
|
||||||
setProgress(complete);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return response;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HasUpdate = {
|
export type HasUpdate = {
|
||||||
@@ -303,10 +280,10 @@ export const revokeAuthentication = async (): Promise<{ authenticated: Authentic
|
|||||||
*/
|
*/
|
||||||
export const previewRundown = async (
|
export const previewRundown = async (
|
||||||
sheetId: string,
|
sheetId: string,
|
||||||
options: ExcelImportMap,
|
options: ImportMap,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
rundown: OntimeRundown;
|
rundown: OntimeRundown;
|
||||||
userFields: UserFields;
|
customFields: CustomFields;
|
||||||
}> => {
|
}> => {
|
||||||
const response = await axios.post(`${ontimeURL}/sheet/${sheetId}/read`, { options });
|
const response = await axios.post(`${ontimeURL}/sheet/${sheetId}/read`, { options });
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -315,7 +292,7 @@ export const previewRundown = async (
|
|||||||
/**
|
/**
|
||||||
* @description HTTP request to upload the rundown to a google sheet
|
* @description HTTP request to upload the rundown to a google sheet
|
||||||
*/
|
*/
|
||||||
export const uploadRundown = async (sheetId: string, options: ExcelImportMap): Promise<void> => {
|
export const uploadRundown = async (sheetId: string, options: ImportMap): Promise<void> => {
|
||||||
const response = await axios.post(`${ontimeURL}/sheet/${sheetId}/write`, { options });
|
const response = await axios.post(`${ontimeURL}/sheet/${sheetId}/write`, { options });
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
@@ -371,11 +348,17 @@ export async function createProject(
|
|||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests list of known custom fields
|
||||||
|
*/
|
||||||
export async function getCustomFields(): Promise<CustomFields> {
|
export async function getCustomFields(): Promise<CustomFields> {
|
||||||
const res = await axios.get(`${projectDataURL}/custom-field`);
|
const res = await axios.get(`${projectDataURL}/custom-field`);
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets list of known custom fields
|
||||||
|
*/
|
||||||
export async function postCustomField(newField: CustomField): Promise<CustomFields> {
|
export async function postCustomField(newField: CustomField): Promise<CustomFields> {
|
||||||
const res = await axios.post(`${projectDataURL}/custom-field`, {
|
const res = await axios.post(`${projectDataURL}/custom-field`, {
|
||||||
...newField,
|
...newField,
|
||||||
@@ -383,6 +366,9 @@ export async function postCustomField(newField: CustomField): Promise<CustomFiel
|
|||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits single custom field
|
||||||
|
*/
|
||||||
export async function editCustomField(label: CustomFieldLabel, newField: CustomField): Promise<CustomFields> {
|
export async function editCustomField(label: CustomFieldLabel, newField: CustomField): Promise<CustomFields> {
|
||||||
const res = await axios.put(`${projectDataURL}/custom-field/${label}`, {
|
const res = await axios.put(`${projectDataURL}/custom-field/${label}`, {
|
||||||
...newField,
|
...newField,
|
||||||
@@ -390,6 +376,9 @@ export async function editCustomField(label: CustomFieldLabel, newField: CustomF
|
|||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes single custom field
|
||||||
|
*/
|
||||||
export async function deleteCustomField(label: CustomFieldLabel): Promise<CustomFields> {
|
export async function deleteCustomField(label: CustomFieldLabel): Promise<CustomFields> {
|
||||||
const res = await axios.delete(`${projectDataURL}/custom-field/${label}`);
|
const res = await axios.delete(`${projectDataURL}/custom-field/${label}`);
|
||||||
return res.data;
|
return res.data;
|
||||||
|
|||||||
+2
-4
@@ -1,11 +1,9 @@
|
|||||||
.tag {
|
.tag {
|
||||||
font-size: calc(1rem - 3px);
|
font-size: calc(1rem - 3px);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
background-color: $gray-100;
|
background-color: $gray-900;
|
||||||
color: $ui-black;
|
color: $ui-white;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0 0.25rem;
|
padding: 0 0.25rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { queryRefetchInterval } from '../../ontimeConfig';
|
|
||||||
import { USERFIELDS } from '../api/apiConstants';
|
|
||||||
import { getUserFields } from '../api/ontimeApi';
|
|
||||||
import { userFieldsPlaceholder } from '../models/UserFields';
|
|
||||||
|
|
||||||
export default function useUserFields() {
|
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
|
||||||
queryKey: USERFIELDS,
|
|
||||||
queryFn: getUserFields,
|
|
||||||
placeholderData: userFieldsPlaceholder,
|
|
||||||
retry: 5,
|
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
|
||||||
refetchInterval: queryRefetchInterval,
|
|
||||||
networkMode: 'always',
|
|
||||||
});
|
|
||||||
|
|
||||||
return { data, status, isFetching, isError, refetch };
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { UserFields } from 'ontime-types';
|
|
||||||
|
|
||||||
export const userFieldsPlaceholder: UserFields = {
|
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { EndAction, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
import { EndAction, EventCustomFields, OntimeEvent, SupportedEvent, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { cloneEvent } from '../eventsManager';
|
import { cloneEvent } from '../eventsManager';
|
||||||
|
|
||||||
@@ -21,18 +21,11 @@ describe('cloneEvent()', () => {
|
|||||||
skip: false,
|
skip: false,
|
||||||
colour: 'F00',
|
colour: 'F00',
|
||||||
revision: 10,
|
revision: 10,
|
||||||
user0: 'user0',
|
|
||||||
user1: 'user1',
|
|
||||||
user2: 'user2',
|
|
||||||
user3: 'user3',
|
|
||||||
user4: 'user4',
|
|
||||||
user5: 'user5',
|
|
||||||
user6: 'user6',
|
|
||||||
user7: 'user7',
|
|
||||||
user8: 'user8',
|
|
||||||
user9: 'user9',
|
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
timeDanger: 60000,
|
timeDanger: 60000,
|
||||||
|
custom: {
|
||||||
|
lighting: { value: '3' },
|
||||||
|
} as EventCustomFields,
|
||||||
} as OntimeEvent;
|
} as OntimeEvent;
|
||||||
|
|
||||||
const cloned = cloneEvent(original);
|
const cloned = cloneEvent(original);
|
||||||
@@ -55,5 +48,6 @@ describe('cloneEvent()', () => {
|
|||||||
expect(cloned.revision).toBe(0);
|
expect(cloned.revision).toBe(0);
|
||||||
expect(cloned.timeWarning).toBe(original.timeWarning);
|
expect(cloned.timeWarning).toBe(original.timeWarning);
|
||||||
expect(cloned.timeDanger).toBe(original.timeDanger);
|
expect(cloned.timeDanger).toBe(original.timeDanger);
|
||||||
|
expect(cloned.custom).toStrictEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,22 +6,7 @@ import { OntimeEvent, SupportedEvent } from 'ontime-types';
|
|||||||
* @param {string} [after]
|
* @param {string} [after]
|
||||||
* @return {OntimeEvent} clean event
|
* @return {OntimeEvent} clean event
|
||||||
*/
|
*/
|
||||||
type ClonedEvent = Omit<
|
type ClonedEvent = Omit<OntimeEvent, 'id' | 'cue'>;
|
||||||
OntimeEvent,
|
|
||||||
| 'id'
|
|
||||||
| 'cue'
|
|
||||||
| 'user0'
|
|
||||||
| 'user1'
|
|
||||||
| 'user2'
|
|
||||||
| 'user3'
|
|
||||||
| 'user4'
|
|
||||||
| 'user5'
|
|
||||||
| 'user6'
|
|
||||||
| 'user7'
|
|
||||||
| 'user8'
|
|
||||||
| 'user9'
|
|
||||||
| 'custom'
|
|
||||||
>;
|
|
||||||
export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
||||||
return {
|
return {
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
@@ -43,5 +28,6 @@ export const cloneEvent = (event: OntimeEvent, after?: string): ClonedEvent => {
|
|||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: event.timeWarning,
|
timeWarning: event.timeWarning,
|
||||||
timeDanger: event.timeDanger,
|
timeDanger: event.timeDanger,
|
||||||
|
custom: {},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+38
-24
@@ -1,6 +1,42 @@
|
|||||||
import { ExcelImportMap } from 'ontime-utils';
|
/**
|
||||||
|
* Collection of rules for pre-validating a spreadsheet
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
export function validateSpreadsheetImport(file: File) {
|
||||||
|
if (!isExcelFile(file)) {
|
||||||
|
throw new Error('Unknown file type');
|
||||||
|
}
|
||||||
|
|
||||||
import { ProjectFileImportOptions } from '../../../common/api/ontimeApi';
|
// Check if file is empty
|
||||||
|
if (file.size === 0) {
|
||||||
|
throw new Error('File is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit file size of an excel file to around 10MB
|
||||||
|
if (file.size > 10_000_000) {
|
||||||
|
throw new Error('File size limit (10MB) exceeded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of rules for pre-validating a project file
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
export function validateProjectFile(file: File) {
|
||||||
|
if (!isOntimeFile(file)) {
|
||||||
|
throw new Error('Unknown file type');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file is empty
|
||||||
|
if (file.size === 0) {
|
||||||
|
throw new Error('File is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit file size of a project file to around 1MB
|
||||||
|
if (file.size > 1_000_000) {
|
||||||
|
throw new Error('File size limit (10MB) exceeded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates a file according to the app upload contract
|
* Validates a file according to the app upload contract
|
||||||
@@ -40,25 +76,3 @@ export function isExcelFile(file: File | null) {
|
|||||||
export function isOntimeFile(file: File | null) {
|
export function isOntimeFile(file: File | null) {
|
||||||
return file?.name.endsWith('.json');
|
return file?.name.endsWith('.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
type PersistedOntimeOptions = {
|
|
||||||
optionType: 'ontime';
|
|
||||||
options: Partial<ProjectFileImportOptions>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PersistedExcelOptions = {
|
|
||||||
optionType: 'excel';
|
|
||||||
options: ExcelImportMap;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function persistOptions(options: PersistedOntimeOptions | PersistedExcelOptions) {
|
|
||||||
localStorage.setItem(`ontime-import-options-${options.optionType}`, JSON.stringify(options.options));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getPersistedOptions(optionType: 'excel' | 'ontime') {
|
|
||||||
const options = localStorage.getItem(`ontime-import-options-${optionType}`);
|
|
||||||
if (!options) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return JSON.parse(options);
|
|
||||||
}
|
|
||||||
@@ -30,6 +30,9 @@ $inner-padding: 1rem;
|
|||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
font-size: calc(1rem - 1px);
|
font-size: calc(1rem - 1px);
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paragraph {
|
.paragraph {
|
||||||
@@ -60,15 +63,25 @@ $inner-padding: 1rem;
|
|||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 3;
|
||||||
|
box-shadow: 0 1px $white-10;
|
||||||
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
border-bottom: 1px solid $white-10;
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: $gray-400;
|
color: $gray-400;
|
||||||
|
background-color: $gray-1350;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
th,
|
th,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { HTMLAttributes, ReactNode } from 'react';
|
import { HTMLAttributes, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { cx } from '../../../common/utils/styleUtils';
|
||||||
|
|
||||||
import style from './Panel.module.scss';
|
import style from './Panel.module.scss';
|
||||||
|
|
||||||
export function Header({ children }: { children: ReactNode }) {
|
export function Header({ children }: { children: ReactNode }) {
|
||||||
@@ -41,10 +43,11 @@ export function Card({ children, ...props }: { children: ReactNode } & JSX.Intri
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Table({ children }: { children: ReactNode }) {
|
export function Table({ className, children }: { className?: string; children: ReactNode }) {
|
||||||
|
const classes = cx([style.table, className]);
|
||||||
return (
|
return (
|
||||||
<div className={style.pad}>
|
<div className={style.pad}>
|
||||||
<table className={style.table}>{children}</table>
|
<table className={classes}>{children}</table>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function ProjectList({ isCreatingProject, onToggleCreate }: Proje
|
|||||||
<Panel.Table>
|
<Panel.Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Project Name</th>
|
<th className={style.containCell}>Project Name</th>
|
||||||
<th>Date Created</th>
|
<th>Date Created</th>
|
||||||
<th>Date Modified</th>
|
<th>Date Modified</th>
|
||||||
<th />
|
<th />
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export default function ProjectListItem({
|
|||||||
</td>
|
</td>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<td>{filename}</td>
|
<td className={style.containCell}>{filename}</td>
|
||||||
<td>{new Date(createdAt).toLocaleString()}</td>
|
<td>{new Date(createdAt).toLocaleString()}</td>
|
||||||
<td>{new Date(updatedAt).toLocaleString()}</td>
|
<td>{new Date(updatedAt).toLocaleString()}</td>
|
||||||
<td className={style.actionButton}>
|
<td className={style.actionButton}>
|
||||||
|
|||||||
@@ -54,4 +54,8 @@
|
|||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: $red-500;
|
color: $red-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.containCell {
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@ import * as Panel from '../PanelUtils';
|
|||||||
import CustomFieldEntry from './CustomFieldEntry';
|
import CustomFieldEntry from './CustomFieldEntry';
|
||||||
import CustomFieldForm from './CustomFieldForm';
|
import CustomFieldForm from './CustomFieldForm';
|
||||||
|
|
||||||
const userFieldsDocsUrl = 'https://ontime.gitbook.io/v2/features/user-fields';
|
const customFieldsDocsUrl = 'https://ontime.gitbook.io/v2/features/user-fields';
|
||||||
|
|
||||||
export default function ProjectSettingsPanel() {
|
export default function ProjectSettingsPanel() {
|
||||||
const { data, refetch } = useCustomFields();
|
const { data, refetch } = useCustomFields();
|
||||||
@@ -64,7 +64,7 @@ export default function ProjectSettingsPanel() {
|
|||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
This data is not used by Ontime.
|
This data is not used by Ontime.
|
||||||
<ExternalLink href={userFieldsDocsUrl}>See the docs</ExternalLink>
|
<ExternalLink href={customFieldsDocsUrl}>See the docs</ExternalLink>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ interface GSheetSetupProps {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GSheetSetup({ onCancel }: GSheetSetupProps) {
|
export default function GSheetSetup(props: GSheetSetupProps) {
|
||||||
|
const { onCancel } = props;
|
||||||
|
|
||||||
const { revoke, connect, verifyAuth } = useGoogleSheet();
|
const { revoke, connect, verifyAuth } = useGoogleSheet();
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [authKey, setAuthKey] = useState<string | null>(null);
|
const [authKey, setAuthKey] = useState<string | null>(null);
|
||||||
@@ -34,6 +36,10 @@ export default function GSheetSetup({ onCancel }: GSheetSetupProps) {
|
|||||||
const result = await verifyAuth();
|
const result = await verifyAuth();
|
||||||
if (result) {
|
if (result) {
|
||||||
setAuthenticationStatus(result.authenticated);
|
setAuthenticationStatus(result.authenticated);
|
||||||
|
// if we are still pending, lets check again in 2seconds
|
||||||
|
if (result.authenticated === 'pending') {
|
||||||
|
setTimeout(getAuthStatus, 2000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -42,11 +48,6 @@ export default function GSheetSetup({ onCancel }: GSheetSetupProps) {
|
|||||||
getAuthStatus();
|
getAuthStatus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCancelFlow = () => {
|
|
||||||
revoke();
|
|
||||||
onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
// user cancels the flow
|
// user cancels the flow
|
||||||
const handleRevoke = async () => {
|
const handleRevoke = async () => {
|
||||||
setLoading('cancel');
|
setLoading('cancel');
|
||||||
@@ -55,6 +56,11 @@ export default function GSheetSetup({ onCancel }: GSheetSetupProps) {
|
|||||||
setLoading('');
|
setLoading('');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCancelFlow = async () => {
|
||||||
|
await handleRevoke();
|
||||||
|
onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets file from input
|
* Gets file from input
|
||||||
* @param event
|
* @param event
|
||||||
@@ -104,85 +110,79 @@ export default function GSheetSetup({ onCancel }: GSheetSetupProps) {
|
|||||||
const canAuthenticate = Boolean(authKey) && Boolean(authLink);
|
const canAuthenticate = Boolean(authKey) && Boolean(authLink);
|
||||||
const isLoading = Boolean(loading);
|
const isLoading = Boolean(loading);
|
||||||
const isAuthenticated = authenticationStatus === 'authenticated';
|
const isAuthenticated = authenticationStatus === 'authenticated';
|
||||||
|
const isAuthenticating = authenticationStatus === 'pending';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Title>
|
<Panel.Title>
|
||||||
Sync with Google Sheet (experimental)
|
Sync with Google Sheet (experimental)
|
||||||
<Button variant='ontime-subtle' size='sm' onClick={handleCancelFlow}>
|
{isAuthenticated ? (
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</Panel.Title>
|
|
||||||
{isAuthenticated ? (
|
|
||||||
<Panel.ListGroup>
|
|
||||||
<Panel.Title>Authenticated</Panel.Title>
|
|
||||||
<Button variant='ontime-subtle' size='sm' onClick={handleRevoke} isLoading={loading === 'cancel'}>
|
<Button variant='ontime-subtle' size='sm' onClick={handleRevoke} isLoading={loading === 'cancel'}>
|
||||||
Revoke Authentication
|
Revoke Authentication
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button variant='ontime-subtle' size='sm' onClick={handleCancelFlow}>
|
||||||
|
Go Back
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Panel.Title>
|
||||||
|
<Panel.ListGroup>
|
||||||
|
<Panel.Description>Upload Client Secret provided by Google</Panel.Description>
|
||||||
|
<Panel.Error>{undefined}</Panel.Error>
|
||||||
|
<Input
|
||||||
|
type='file'
|
||||||
|
onChange={handleClientSecret}
|
||||||
|
accept='.json'
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
isDisabled={isLoading || canAuthenticate}
|
||||||
|
/>
|
||||||
|
</Panel.ListGroup>
|
||||||
|
<Panel.ListGroup>
|
||||||
|
<Panel.Description>Enter ID of sheet to synchronise</Panel.Description>
|
||||||
|
<Panel.Error>{undefined}</Panel.Error>
|
||||||
|
<Input
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
autoComplete='off'
|
||||||
|
placeholder='Sheet ID'
|
||||||
|
onChange={(event) => setSheetId(event.target.value)}
|
||||||
|
isDisabled={isLoading || canAuthenticate}
|
||||||
|
/>
|
||||||
|
</Panel.ListGroup>
|
||||||
|
{!canAuthenticate ? (
|
||||||
|
<Panel.ListGroup>
|
||||||
|
<div className={style.buttonRow}>
|
||||||
|
<Button
|
||||||
|
variant='ontime-subtle'
|
||||||
|
size='sm'
|
||||||
|
leftIcon={<IoCheckmark />}
|
||||||
|
onClick={handleConnect}
|
||||||
|
isDisabled={!canConnect || isLoading}
|
||||||
|
isLoading={loading === 'connect'}
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<Panel.ListGroup>
|
||||||
<Panel.ListGroup>
|
<div className={style.buttonRow}>
|
||||||
<Panel.Description>Upload Client Secret provided by Google</Panel.Description>
|
<CopyTag label='Google Auth Key' disabled={!canAuthenticate} size='sm'>
|
||||||
<Panel.Error>{undefined}</Panel.Error>
|
{authKey ? authKey : 'Upload files to generate Auth Key'}
|
||||||
<Input
|
</CopyTag>
|
||||||
type='file'
|
<Button
|
||||||
onChange={handleClientSecret}
|
|
||||||
accept='.json'
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-filled'
|
variant='ontime-filled'
|
||||||
isDisabled={isLoading || canAuthenticate}
|
|
||||||
/>
|
|
||||||
</Panel.ListGroup>
|
|
||||||
|
|
||||||
<Panel.ListGroup>
|
|
||||||
<Panel.Description>Enter ID of sheet to synchronise</Panel.Description>
|
|
||||||
<Panel.Error>{undefined}</Panel.Error>
|
|
||||||
<Input
|
|
||||||
size='sm'
|
size='sm'
|
||||||
variant='ontime-filled'
|
leftIcon={<IoShieldCheckmarkOutline />}
|
||||||
autoComplete='off'
|
onClick={handleAuthenticate}
|
||||||
placeholder='Sheet ID'
|
isDisabled={!canAuthenticate || isLoading}
|
||||||
onChange={(event) => setSheetId(event.target.value)}
|
isLoading={loading === 'authenticate' || isAuthenticating}
|
||||||
isDisabled={isLoading || canAuthenticate}
|
>
|
||||||
/>
|
Authenticate
|
||||||
</Panel.ListGroup>
|
</Button>
|
||||||
|
</div>
|
||||||
{!canAuthenticate ? (
|
</Panel.ListGroup>
|
||||||
<Panel.ListGroup>
|
|
||||||
<div className={style.buttonRow}>
|
|
||||||
<Button
|
|
||||||
variant='ontime-subtle'
|
|
||||||
size='sm'
|
|
||||||
leftIcon={<IoCheckmark />}
|
|
||||||
onClick={handleConnect}
|
|
||||||
isDisabled={!canConnect || isLoading}
|
|
||||||
isLoading={loading === 'connect'}
|
|
||||||
>
|
|
||||||
Connect
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Panel.ListGroup>
|
|
||||||
) : (
|
|
||||||
<Panel.ListGroup>
|
|
||||||
<div className={style.buttonRow}>
|
|
||||||
<CopyTag label='Google Auth Key' disabled={!canAuthenticate} size='sm'>
|
|
||||||
{authKey ? authKey : 'Upload files to generate Auth Key'}
|
|
||||||
</CopyTag>
|
|
||||||
<Button
|
|
||||||
variant='ontime-filled'
|
|
||||||
size='sm'
|
|
||||||
leftIcon={<IoShieldCheckmarkOutline />}
|
|
||||||
onClick={handleAuthenticate}
|
|
||||||
isDisabled={!canAuthenticate || isLoading}
|
|
||||||
isLoading={loading === 'authenticate'}
|
|
||||||
>
|
|
||||||
Authenticate
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Panel.ListGroup>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Button } from '@chakra-ui/react';
|
|
||||||
|
|
||||||
import ExcelFileOptions from '../../../modals/upload-modal/upload-options/ExcelFileOptions';
|
|
||||||
import * as Panel from '../PanelUtils';
|
|
||||||
|
|
||||||
import useGoogleSheet from './useGoogleSheet';
|
|
||||||
import { useSheetStore } from './useSheetStore';
|
|
||||||
|
|
||||||
import style from './SourcesPanel.module.scss';
|
|
||||||
|
|
||||||
export default function ImportMap() {
|
|
||||||
const { importRundownPreview, exportRundown } = useGoogleSheet();
|
|
||||||
|
|
||||||
const importOptions = useSheetStore((state) => state.excelFileOptions);
|
|
||||||
const patchImportOptions = useSheetStore((state) => state.patchExcelFileOptions);
|
|
||||||
const stepData = useSheetStore((state) => state.stepData);
|
|
||||||
const sheetId = useSheetStore((state) => state.sheetId);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState<'' | 'export' | 'import'>('');
|
|
||||||
|
|
||||||
const handleExport = async () => {
|
|
||||||
if (!sheetId) return;
|
|
||||||
setLoading('export');
|
|
||||||
await exportRundown(sheetId, importOptions);
|
|
||||||
setLoading('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImportPreview = async () => {
|
|
||||||
if (!sheetId) return;
|
|
||||||
setLoading('import');
|
|
||||||
await importRundownPreview(sheetId, importOptions);
|
|
||||||
setLoading('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const isLoading = Boolean(loading);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Panel.Section>
|
|
||||||
<Panel.Title>Import options</Panel.Title>
|
|
||||||
<ExcelFileOptions importOptions={importOptions} updateOptions={patchImportOptions} />
|
|
||||||
<Panel.Error>{stepData.worksheet.error}</Panel.Error>
|
|
||||||
<div className={style.buttonRow}>
|
|
||||||
<Button
|
|
||||||
variant='ontime-filled'
|
|
||||||
size='sm'
|
|
||||||
onClick={handleExport}
|
|
||||||
isDisabled={isLoading || !sheetId}
|
|
||||||
isLoading={loading === 'export'}
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant='ontime-filled'
|
|
||||||
size='sm'
|
|
||||||
onClick={handleImportPreview}
|
|
||||||
isDisabled={isLoading || !sheetId}
|
|
||||||
isLoading={loading === 'import'}
|
|
||||||
>
|
|
||||||
Import preview
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Panel.Section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { Button } from '@chakra-ui/react';
|
import { Button } from '@chakra-ui/react';
|
||||||
import { OntimeRundown, UserFields } from 'ontime-types';
|
import { CustomFields, OntimeRundown } from 'ontime-types';
|
||||||
|
|
||||||
import PreviewExcel from '../../../modals/upload-modal/preview/PreviewExcel';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
|
import PreviewSpreadsheet from './preview/PreviewRundown';
|
||||||
import useGoogleSheet from './useGoogleSheet';
|
import useGoogleSheet from './useGoogleSheet';
|
||||||
import { useSheetStore } from './useSheetStore';
|
import { useSheetStore } from './useSheetStore';
|
||||||
|
|
||||||
@@ -10,28 +12,43 @@ import style from './SourcesPanel.module.scss';
|
|||||||
|
|
||||||
interface ImportReviewProps {
|
interface ImportReviewProps {
|
||||||
rundown: OntimeRundown;
|
rundown: OntimeRundown;
|
||||||
userFields: UserFields;
|
customFields: CustomFields;
|
||||||
|
onFinished: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ImportReview({ rundown, userFields }: ImportReviewProps) {
|
export default function ImportReview(props: ImportReviewProps) {
|
||||||
|
const { rundown, customFields, onFinished } = props;
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const { importRundown } = useGoogleSheet();
|
const { importRundown } = useGoogleSheet();
|
||||||
const resetPreview = useSheetStore((state) => state.resetPreview);
|
const resetPreview = useSheetStore((state) => state.resetPreview);
|
||||||
|
|
||||||
const applyImport = () => {
|
const handleCancel = () => {
|
||||||
importRundown(rundown, userFields);
|
resetPreview();
|
||||||
|
onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyImport = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await importRundown(rundown, customFields);
|
||||||
|
setLoading(false);
|
||||||
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Panel.Section>
|
||||||
<PreviewExcel rundown={rundown} userFields={userFields} />
|
<Panel.Title>
|
||||||
<div className={style.buttonRow}>
|
Review Rundown
|
||||||
<Button onClick={resetPreview} variant='ontime-ghosted' size='sm'>
|
<div className={style.buttonRow}>
|
||||||
Cancel
|
<Button onClick={handleCancel} variant='ontime-ghosted' size='sm' isDisabled={loading}>
|
||||||
</Button>
|
Cancel
|
||||||
<Button onClick={applyImport} variant='ontime-filled' size='sm'>
|
</Button>
|
||||||
Apply
|
<Button onClick={applyImport} variant='ontime-filled' size='sm' isLoading={loading}>
|
||||||
</Button>
|
Apply
|
||||||
</div>
|
</Button>
|
||||||
</>
|
</div>
|
||||||
|
</Panel.Title>
|
||||||
|
<PreviewSpreadsheet rundown={rundown} customFields={customFields} />
|
||||||
|
</Panel.Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,3 +20,8 @@
|
|||||||
.inputContainer {
|
.inputContainer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.singleActionCell {
|
||||||
|
width: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,35 +1,61 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { ChangeEvent, useRef, useState } from 'react';
|
||||||
import { Button, Input } from '@chakra-ui/react';
|
import { Button, Input } from '@chakra-ui/react';
|
||||||
import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
|
import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
|
||||||
import { IoDownloadOutline } from '@react-icons/all-files/io5/IoDownloadOutline';
|
import { IoDownloadOutline } from '@react-icons/all-files/io5/IoDownloadOutline';
|
||||||
|
import { ImportMap, unpackError } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
||||||
|
import { importSpreadsheetPreview } from '../../../../common/api/ontimeApi';
|
||||||
|
import { validateSpreadsheetImport } from '../../../../common/utils/uploadUtils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
|
import ImportMapForm from './import-map/ImportMapForm';
|
||||||
import GSheetInfo from './GSheetInfo';
|
import GSheetInfo from './GSheetInfo';
|
||||||
import GSheetSetup from './GSheetSetup';
|
import GSheetSetup from './GSheetSetup';
|
||||||
import ImportMap from './ImportMap';
|
|
||||||
import ImportReview from './ImportReview';
|
import ImportReview from './ImportReview';
|
||||||
|
import useGoogleSheet from './useGoogleSheet';
|
||||||
import { useSheetStore } from './useSheetStore';
|
import { useSheetStore } from './useSheetStore';
|
||||||
|
|
||||||
import style from './SourcesPanel.module.scss';
|
import style from './SourcesPanel.module.scss';
|
||||||
|
|
||||||
export default function SourcesPanel() {
|
export default function SourcesPanel() {
|
||||||
const [importFlow, setImportFlow] = useState<'none' | 'excel' | 'gsheet'>('none');
|
const [importFlow, setImportFlow] = useState<'none' | 'excel' | 'gsheet'>('none');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
const { exportRundown, importRundownPreview, revoke, verifyAuth } = useGoogleSheet();
|
||||||
|
|
||||||
|
const spreadsheet = useSheetStore((state) => state.spreadsheet);
|
||||||
|
const setSpreadsheet = useSheetStore((state) => state.setSpreadsheet);
|
||||||
const authenticationStatus = useSheetStore((state) => state.authenticationStatus);
|
const authenticationStatus = useSheetStore((state) => state.authenticationStatus);
|
||||||
|
const setAuthenticationStatus = useSheetStore((state) => state.setAuthenticationStatus);
|
||||||
const rundown = useSheetStore((state) => state.rundown);
|
const rundown = useSheetStore((state) => state.rundown);
|
||||||
const userFields = useSheetStore((state) => state.userFields);
|
const setRundown = useSheetStore((state) => state.setRundown);
|
||||||
|
const customFields = useSheetStore((state) => state.customFields);
|
||||||
const isAuthenticated = authenticationStatus === 'authenticated';
|
const setCustomFields = useSheetStore((state) => state.setCustomFields);
|
||||||
const hasData = rundown && userFields;
|
const sheetId = useSheetStore((state) => state.sheetId);
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleFile = () => console.error('not yet implemented');
|
const handleFile = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const fileToUpload = event.target.files?.[0];
|
||||||
|
|
||||||
|
if (!fileToUpload) {
|
||||||
|
setSpreadsheet(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
validateSpreadsheetImport(fileToUpload);
|
||||||
|
setSpreadsheet(fileToUpload);
|
||||||
|
setImportFlow('excel');
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = unpackError(error);
|
||||||
|
setError(`Error uploading file: ${errorMessage}`);
|
||||||
|
setSpreadsheet(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpload = () => {
|
const handleUpload = () => {
|
||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
setImportFlow('excel');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openGSheetFlow = () => {
|
const openGSheetFlow = () => {
|
||||||
@@ -40,28 +66,81 @@ export default function SourcesPanel() {
|
|||||||
setImportFlow('none');
|
setImportFlow('none');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmitImportPreview = async (importMap: ImportMap) => {
|
||||||
|
if (importFlow === 'excel') {
|
||||||
|
if (!spreadsheet) return;
|
||||||
|
try {
|
||||||
|
const previewData = await importSpreadsheetPreview(spreadsheet, importMap);
|
||||||
|
setRundown(previewData.rundown);
|
||||||
|
setCustomFields(previewData.customFields);
|
||||||
|
} catch (error) {
|
||||||
|
setError(maybeAxiosError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importFlow === 'gsheet') {
|
||||||
|
if (!sheetId) return;
|
||||||
|
await importRundownPreview(sheetId, importMap);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelImportMap = async () => {
|
||||||
|
setImportFlow('none');
|
||||||
|
if (spreadsheet) {
|
||||||
|
setSpreadsheet(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authenticationStatus === 'authenticated') {
|
||||||
|
await revoke();
|
||||||
|
const result = await verifyAuth();
|
||||||
|
if (result) {
|
||||||
|
setAuthenticationStatus(result.authenticated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleFinished = () => {
|
||||||
|
setImportFlow('none');
|
||||||
|
setRundown(null);
|
||||||
|
setSpreadsheet(null);
|
||||||
|
setCustomFields(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitExport = async (importMap: ImportMap) => {
|
||||||
|
if (!sheetId) return;
|
||||||
|
await exportRundown(sheetId, importMap);
|
||||||
|
};
|
||||||
|
|
||||||
const isExcelFlow = importFlow === 'excel';
|
const isExcelFlow = importFlow === 'excel';
|
||||||
const isGSheetFlow = importFlow === 'gsheet';
|
const isGSheetFlow = importFlow === 'gsheet';
|
||||||
|
const hasFile = Boolean(spreadsheet);
|
||||||
|
const isAuthenticated = authenticationStatus === 'authenticated';
|
||||||
|
const showInput = importFlow === 'none';
|
||||||
|
const showAuth = isGSheetFlow && !isAuthenticated;
|
||||||
|
const showImportMap = (isGSheetFlow && isAuthenticated) || (isExcelFlow && hasFile);
|
||||||
|
const showReview = rundown !== null && customFields !== null;
|
||||||
|
|
||||||
|
console.log(isAuthenticated);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.Header>Data sources</Panel.Header>
|
<Panel.Header>Data sources</Panel.Header>
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
<Panel.Card>
|
<Panel.Card>
|
||||||
<Panel.SubHeader>Synchronise your rundown with an external source</Panel.SubHeader>
|
<Panel.SubHeader>Synchronise your rundown with an external source</Panel.SubHeader>
|
||||||
<GSheetInfo />
|
{error && <Panel.Error>{error}</Panel.Error>}
|
||||||
{!isGSheetFlow && (
|
{showInput && (
|
||||||
<>
|
<>
|
||||||
<Input ref={fileInputRef} style={{ display: 'none' }} type='file' onChange={handleFile} accept='.xlsx' />
|
<GSheetInfo />
|
||||||
|
<Input
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
type='file'
|
||||||
|
onChange={handleFile}
|
||||||
|
accept='.xlsx'
|
||||||
|
data-testid='file-input'
|
||||||
|
/>
|
||||||
<div className={style.uploadSection}>
|
<div className={style.uploadSection}>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button variant='ontime-filled' size='sm' leftIcon={<IoDownloadOutline />} onClick={handleUpload}>
|
||||||
variant='ontime-filled'
|
|
||||||
size='sm'
|
|
||||||
leftIcon={<IoDownloadOutline />}
|
|
||||||
onClick={handleUpload}
|
|
||||||
isDisabled
|
|
||||||
>
|
|
||||||
Import from spreadsheet
|
Import from spreadsheet
|
||||||
</Button>
|
</Button>
|
||||||
<Panel.Description>Accepts .xlsx files</Panel.Description>
|
<Panel.Description>Accepts .xlsx files</Panel.Description>
|
||||||
@@ -75,10 +154,16 @@ export default function SourcesPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isGSheetFlow && <GSheetSetup onCancel={cancelGSheetFlow} />}
|
{showAuth && <GSheetSetup onCancel={cancelGSheetFlow} />}
|
||||||
{isExcelFlow && <Panel.Title>Not yet implemented</Panel.Title>}
|
{showImportMap && !showReview && (
|
||||||
{isAuthenticated && <ImportMap />}
|
<ImportMapForm
|
||||||
{hasData && <ImportReview rundown={rundown} userFields={userFields} />}
|
isSpreadsheet={isExcelFlow}
|
||||||
|
onCancel={cancelImportMap}
|
||||||
|
onSubmitExport={handleSubmitExport}
|
||||||
|
onSubmitImport={handleSubmitImportPreview}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showReview && <ImportReview rundown={rundown} customFields={customFields} onFinished={handleFinished} />}
|
||||||
</Panel.Card>
|
</Panel.Card>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
|
import { Button, IconButton, Input } from '@chakra-ui/react';
|
||||||
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
|
import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||||
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { isAlphanumeric } from '../../../../../common/utils/regex';
|
||||||
|
import * as Panel from '../../PanelUtils';
|
||||||
|
import { useSheetStore } from '../useSheetStore';
|
||||||
|
|
||||||
|
import { convertToImportMap, getPersistedOptions, NamedImportMap, persistImportMap } from './importMapUtils';
|
||||||
|
|
||||||
|
import style from '../SourcesPanel.module.scss';
|
||||||
|
|
||||||
|
interface ImportMapFormProps {
|
||||||
|
isSpreadsheet?: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSubmitExport: (importMap: ImportMap) => Promise<void>;
|
||||||
|
onSubmitImport: (importMap: ImportMap) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImportMapForm(props: ImportMapFormProps) {
|
||||||
|
const { isSpreadsheet, onCancel, onSubmitExport, onSubmitImport } = props;
|
||||||
|
const namedImportMap = getPersistedOptions();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
formState: { errors, isValid },
|
||||||
|
} = useForm<NamedImportMap>({
|
||||||
|
mode: 'onBlur',
|
||||||
|
defaultValues: namedImportMap,
|
||||||
|
values: namedImportMap,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'custom',
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepData = useSheetStore((state) => state.stepData);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<'' | 'export' | 'import'>('');
|
||||||
|
|
||||||
|
const handleExport = async (values: NamedImportMap) => {
|
||||||
|
setLoading('export');
|
||||||
|
const importMap = convertToImportMap(values);
|
||||||
|
|
||||||
|
await onSubmitExport(importMap);
|
||||||
|
setLoading('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportPreview = async (values: NamedImportMap) => {
|
||||||
|
setLoading('import');
|
||||||
|
const importMap = convertToImportMap(values);
|
||||||
|
persistImportMap(values);
|
||||||
|
await onSubmitImport(importMap);
|
||||||
|
setLoading('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCustomImport = (index: number) => {
|
||||||
|
remove(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addCustomImport = () => {
|
||||||
|
append({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = Boolean(loading);
|
||||||
|
const canSubmitSpreadsheet = isSpreadsheet && !isLoading;
|
||||||
|
const canSubmitGSheet = !isLoading;
|
||||||
|
const canSubmit = isValid && (canSubmitSpreadsheet || canSubmitGSheet);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel.Section as='form' id='import-map'>
|
||||||
|
<Panel.Title>
|
||||||
|
Import options
|
||||||
|
<div className={style.buttonRow}>
|
||||||
|
<Button variant='ontime-subtle' size='sm' onClick={onCancel} isDisabled={isLoading}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{!isSpreadsheet && (
|
||||||
|
<Button
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
onClick={handleSubmit(handleExport)}
|
||||||
|
isDisabled={!canSubmitGSheet}
|
||||||
|
isLoading={loading === 'export'}
|
||||||
|
>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
onClick={handleSubmit(handleImportPreview)}
|
||||||
|
isDisabled={!canSubmit}
|
||||||
|
isLoading={loading === 'import'}
|
||||||
|
>
|
||||||
|
Import preview
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Panel.Title>
|
||||||
|
<Panel.Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Ontime field</th>
|
||||||
|
<th>From spreadsheet name</th>
|
||||||
|
<th className={style.singleActionCell} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Object.entries(namedImportMap).map(([label, importName]) => {
|
||||||
|
if (label === 'custom') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<tr key={importName as string}>
|
||||||
|
<td>{label}</td>
|
||||||
|
<td>
|
||||||
|
<Input
|
||||||
|
id={importName as string}
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
autoComplete='off'
|
||||||
|
maxLength={25}
|
||||||
|
defaultValue={importName as string}
|
||||||
|
placeholder='Use default column name'
|
||||||
|
{...register(label as keyof NamedImportMap)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className={style.singleActionCell} />
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
const ontimeName = field.ontimeName;
|
||||||
|
const importName = field.importName;
|
||||||
|
const maybeOntimeError = errors.custom?.[index]?.ontimeName?.message;
|
||||||
|
const key = `custom.${index}.ontimeName`;
|
||||||
|
return (
|
||||||
|
<tr key={key}>
|
||||||
|
<td>
|
||||||
|
<Input
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
autoComplete='off'
|
||||||
|
maxLength={25}
|
||||||
|
defaultValue={ontimeName}
|
||||||
|
placeholder='Name of the field as shown in Ontime'
|
||||||
|
{...register(`custom.${index}.ontimeName`, {
|
||||||
|
pattern: {
|
||||||
|
value: isAlphanumeric,
|
||||||
|
message: 'Custom field name must be alphanumeric',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{maybeOntimeError && <Panel.Error>{maybeOntimeError}</Panel.Error>}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Input
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-filled'
|
||||||
|
autoComplete='off'
|
||||||
|
maxLength={25}
|
||||||
|
defaultValue={importName}
|
||||||
|
placeholder='Name of the column in the spreadsheet'
|
||||||
|
{...register(`custom.${index}.importName`)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className={style.singleActionCell}>
|
||||||
|
<IconButton
|
||||||
|
size='sm'
|
||||||
|
variant='ontime-ghosted'
|
||||||
|
color='#FA5656' // $red-500
|
||||||
|
icon={<IoTrash />}
|
||||||
|
aria-label='Delete entry'
|
||||||
|
onClick={() => deleteCustomImport(index)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<tr>
|
||||||
|
<td />
|
||||||
|
<td className={style.buttonRow} colSpan={99}>
|
||||||
|
<Button size='sm' variant='ontime-subtle' rightIcon={<IoAdd />} onClick={addCustomImport}>
|
||||||
|
Add custom field
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
<td />
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Panel.Table>
|
||||||
|
<Panel.Error>{stepData.worksheet.error}</Panel.Error>
|
||||||
|
</Panel.Section>
|
||||||
|
);
|
||||||
|
}
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
import { ImportCustom } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { convertToImportMap } from '../importMapUtils';
|
||||||
|
|
||||||
|
describe('convertToImportMap', () => {
|
||||||
|
it('converts a namedImportMap to a importMap', () => {
|
||||||
|
const defaultNamedImporMap = {
|
||||||
|
Worksheet: 'event schedule',
|
||||||
|
Start: 'time start',
|
||||||
|
End: 'time end',
|
||||||
|
Duration: 'duration',
|
||||||
|
Cue: 'cue',
|
||||||
|
Title: 'title',
|
||||||
|
Presenter: 'presenter',
|
||||||
|
Subtitle: 'subtitle',
|
||||||
|
'Is Public': 'public',
|
||||||
|
Skip: 'skip',
|
||||||
|
Note: 'notes',
|
||||||
|
Colour: 'colour',
|
||||||
|
'End action': 'end action',
|
||||||
|
'Timer type': 'timer type',
|
||||||
|
'Time warning': 'warning time',
|
||||||
|
'Time danger': 'danger time',
|
||||||
|
custom: [
|
||||||
|
{ ontimeName: 'Custom1 ', importName: 'custom1' },
|
||||||
|
{ ontimeName: 'Custom2', importName: 'custom2' },
|
||||||
|
{ ontimeName: 'Custom3', importName: 'custom3' },
|
||||||
|
{ ontimeName: 'EmptyImportName', importName: '' },
|
||||||
|
{ ontimeName: '', importName: 'EmptyOntimeName' },
|
||||||
|
] as ImportCustom[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const importMap = convertToImportMap(defaultNamedImporMap);
|
||||||
|
expect(importMap.custom).toStrictEqual({
|
||||||
|
Custom1: 'custom1',
|
||||||
|
Custom2: 'custom2',
|
||||||
|
Custom3: 'custom3',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
import { ImportCustom, ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
|
export type NamedImportMap = typeof namedImportMap;
|
||||||
|
|
||||||
|
// Record of label and import name
|
||||||
|
export const namedImportMap = {
|
||||||
|
Worksheet: 'event schedule',
|
||||||
|
Start: 'time start',
|
||||||
|
End: 'time end',
|
||||||
|
Duration: 'duration',
|
||||||
|
Cue: 'cue',
|
||||||
|
Title: 'title',
|
||||||
|
Presenter: 'presenter',
|
||||||
|
Subtitle: 'subtitle',
|
||||||
|
'Is Public': 'public',
|
||||||
|
Skip: 'skip',
|
||||||
|
Note: 'notes',
|
||||||
|
Colour: 'colour',
|
||||||
|
'End action': 'end action',
|
||||||
|
'Timer type': 'timer type',
|
||||||
|
'Time warning': 'warning time',
|
||||||
|
'Time danger': 'danger time',
|
||||||
|
custom: [] as ImportCustom[],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convertToImportMap(namedImportMap: NamedImportMap): ImportMap {
|
||||||
|
const custom = namedImportMap.custom.reduce((accumulator, { ontimeName, importName }) => {
|
||||||
|
if (ontimeName && importName) {
|
||||||
|
accumulator[ontimeName.trim()] = importName.trim();
|
||||||
|
}
|
||||||
|
return accumulator;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
worksheet: namedImportMap.Worksheet,
|
||||||
|
timeStart: namedImportMap.Start,
|
||||||
|
timeEnd: namedImportMap.End,
|
||||||
|
duration: namedImportMap.Duration,
|
||||||
|
cue: namedImportMap.Cue,
|
||||||
|
title: namedImportMap.Title,
|
||||||
|
presenter: namedImportMap.Presenter,
|
||||||
|
subtitle: namedImportMap.Subtitle,
|
||||||
|
isPublic: namedImportMap['Is Public'],
|
||||||
|
skip: namedImportMap.Skip,
|
||||||
|
note: namedImportMap.Note,
|
||||||
|
colour: namedImportMap.Colour,
|
||||||
|
endAction: namedImportMap['End action'],
|
||||||
|
timerType: namedImportMap['Timer type'],
|
||||||
|
timeWarning: namedImportMap['Time warning'],
|
||||||
|
timeDanger: namedImportMap['Time danger'],
|
||||||
|
custom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persistImportMap(options: NamedImportMap) {
|
||||||
|
localStorage.setItem('ontime-import-options', JSON.stringify(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPersistedOptions(): NamedImportMap {
|
||||||
|
const options = localStorage.getItem('ontime-import-options');
|
||||||
|
if (!options) {
|
||||||
|
return namedImportMap;
|
||||||
|
}
|
||||||
|
return JSON.parse(options);
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr .secondaryRow {
|
||||||
|
background-color: $white-7;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import { CustomFields, isOntimeBlock, isOntimeEvent, OntimeRundown } from 'ontime-types';
|
||||||
|
import { millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
|
import Tag from '../../../../../common/components/tag/Tag';
|
||||||
|
import { getAccessibleColour } from '../../../../../common/utils/styleUtils';
|
||||||
|
import * as Panel from '../../PanelUtils';
|
||||||
|
|
||||||
|
import style from './PreviewRundown.module.scss';
|
||||||
|
|
||||||
|
interface PreviewRundownProps {
|
||||||
|
rundown: OntimeRundown;
|
||||||
|
customFields: CustomFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
function booleanToText(value?: boolean) {
|
||||||
|
return value ? 'Yes' : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PreviewRundown(props: PreviewRundownProps) {
|
||||||
|
const { rundown, customFields } = props;
|
||||||
|
|
||||||
|
// we only count Ontime Events which are 1 based in client
|
||||||
|
let eventIndex = 0;
|
||||||
|
|
||||||
|
const fieldHeaders = Object.keys(customFields);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Panel.Table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Cue</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Subtitle</th>
|
||||||
|
<th>Presenter</th>
|
||||||
|
<th>Time Start</th>
|
||||||
|
<th>Time End</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Warning Time</th>
|
||||||
|
<th>Danger Time</th>
|
||||||
|
<th>Is Public</th>
|
||||||
|
<th>Skip</th>
|
||||||
|
<th>Colour</th>
|
||||||
|
<th>Timer Type</th>
|
||||||
|
<th>End Action</th>
|
||||||
|
{fieldHeaders.map((field) => (
|
||||||
|
<th key={field}>{field}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rundown.map((event) => {
|
||||||
|
if (isOntimeBlock(event)) {
|
||||||
|
return (
|
||||||
|
<tr key={event.id}>
|
||||||
|
<td className={style.center}>
|
||||||
|
<Tag>-</Tag>
|
||||||
|
</td>
|
||||||
|
<td className={style.center}>
|
||||||
|
<Tag>{event.type}</Tag>
|
||||||
|
</td>
|
||||||
|
<td />
|
||||||
|
<td colSpan={99}>{event.title}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!isOntimeEvent(event)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
eventIndex += 1;
|
||||||
|
const colour = event.colour ? getAccessibleColour(event.colour) : {};
|
||||||
|
const isPublic = booleanToText(event.isPublic);
|
||||||
|
const skip = booleanToText(event.skip);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={event.id}>
|
||||||
|
<tr>
|
||||||
|
<td className={style.center}>
|
||||||
|
<Tag>{eventIndex}</Tag>
|
||||||
|
</td>
|
||||||
|
<td className={style.center}>
|
||||||
|
<Tag>{event.type}</Tag>
|
||||||
|
</td>
|
||||||
|
<td className={style.nowrap}>{event.cue}</td>
|
||||||
|
<td>{event.title}</td>
|
||||||
|
<td>{event.subtitle}</td>
|
||||||
|
<td>{event.presenter}</td>
|
||||||
|
<td>{millisToString(event.timeStart)}</td>
|
||||||
|
<td>{millisToString(event.timeEnd)}</td>
|
||||||
|
<td>{millisToString(event.duration)}</td>
|
||||||
|
<td>{millisToString(event.timeWarning)}</td>
|
||||||
|
<td>{millisToString(event.timeDanger)}</td>
|
||||||
|
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
|
||||||
|
<td>{skip && <Tag>{skip}</Tag>}</td>
|
||||||
|
<td style={{ ...colour }}>{event.colour}</td>
|
||||||
|
<td className={style.center}>
|
||||||
|
<Tag>{event.timerType}</Tag>
|
||||||
|
</td>
|
||||||
|
<td className={style.center}>
|
||||||
|
<Tag>{event.endAction}</Tag>
|
||||||
|
</td>
|
||||||
|
{isOntimeEvent(event) &&
|
||||||
|
fieldHeaders.map((field) => {
|
||||||
|
let value = '';
|
||||||
|
if (field in event.custom) {
|
||||||
|
value = event.custom[field].value;
|
||||||
|
}
|
||||||
|
return <td key={field}>{value}</td>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
{event.note && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={99} className={style.secondaryRow}>
|
||||||
|
Note: {event.note}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Panel.Table>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { AuthenticationStatus, OntimeRundown, UserFields } from 'ontime-types';
|
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||||
import { ExcelImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { RUNDOWN, USERFIELDS } from '../../../../common/api/apiConstants';
|
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/apiConstants';
|
||||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
||||||
import {
|
import {
|
||||||
patchData,
|
patchData,
|
||||||
@@ -20,7 +20,7 @@ export default function useGoogleSheet() {
|
|||||||
// functions push data to store
|
// functions push data to store
|
||||||
const patchStepData = useSheetStore((state) => state.patchStepData);
|
const patchStepData = useSheetStore((state) => state.patchStepData);
|
||||||
const setRundown = useSheetStore((state) => state.setRundown);
|
const setRundown = useSheetStore((state) => state.setRundown);
|
||||||
const setUserFields = useSheetStore((state) => state.setUserFields);
|
const setCustomFields = useSheetStore((state) => state.setCustomFields);
|
||||||
|
|
||||||
/** whether the current session has been authenticated */
|
/** whether the current session has been authenticated */
|
||||||
const verifyAuth = async (): Promise<{ authenticated: AuthenticationStatus } | void> => {
|
const verifyAuth = async (): Promise<{ authenticated: AuthenticationStatus } | void> => {
|
||||||
@@ -43,6 +43,7 @@ export default function useGoogleSheet() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** requests the revoking of an existing authenticated session */
|
||||||
const revoke = async (): Promise<{ authenticated: AuthenticationStatus } | void> => {
|
const revoke = async (): Promise<{ authenticated: AuthenticationStatus } | void> => {
|
||||||
try {
|
try {
|
||||||
return revokeAuthentication();
|
return revokeAuthentication();
|
||||||
@@ -52,18 +53,18 @@ export default function useGoogleSheet() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** fetches data from a worksheet by its ID */
|
/** fetches data from a worksheet by its ID */
|
||||||
const importRundownPreview = async (sheetId: string, fileOptions: ExcelImportMap) => {
|
const importRundownPreview = async (sheetId: string, fileOptions: ImportMap) => {
|
||||||
try {
|
try {
|
||||||
const data = await previewRundown(sheetId, fileOptions);
|
const data = await previewRundown(sheetId, fileOptions);
|
||||||
setRundown(data.rundown);
|
setRundown(data.rundown);
|
||||||
setUserFields(data.userFields);
|
setCustomFields(data.customFields);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } });
|
patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** writes data to a worksheet by its ID */
|
/** writes data to a worksheet by its ID */
|
||||||
const exportRundown = async (sheetId: string, fileOptions: ExcelImportMap) => {
|
const exportRundown = async (sheetId: string, fileOptions: ImportMap) => {
|
||||||
try {
|
try {
|
||||||
// write data to google
|
// write data to google
|
||||||
await uploadRundown(sheetId, fileOptions);
|
await uploadRundown(sheetId, fileOptions);
|
||||||
@@ -73,14 +74,14 @@ export default function useGoogleSheet() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** applies rundown and userfields to current project */
|
/** applies rundown and customFields to current project */
|
||||||
const importRundown = async (rundown: OntimeRundown, userFields: UserFields) => {
|
const importRundown = async (rundown: OntimeRundown, customFields: CustomFields) => {
|
||||||
try {
|
try {
|
||||||
await patchData({ rundown, userFields });
|
await patchData({ rundown, customFields });
|
||||||
// we are unable to optimistically set the rundown since we need
|
// we are unable to optimistically set the rundown since we need
|
||||||
// it to be normalised
|
// it to be normalised
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: [...RUNDOWN, ...USERFIELDS],
|
queryKey: [RUNDOWN, CUSTOM_FIELDS],
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } });
|
patchStepData({ pullPush: { available: true, error: maybeAxiosError(error) } });
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import { AuthenticationStatus, OntimeRundown, UserFields } from 'ontime-types';
|
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||||
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
|
import { defaultImportMap, ImportMap } from 'ontime-utils';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
// TODO: persist excelFileOptions to localStorage
|
|
||||||
|
|
||||||
type SheetStore = {
|
type SheetStore = {
|
||||||
stepData: typeof initialStepData;
|
stepData: typeof initialStepData;
|
||||||
patchStepData: (patch: Partial<typeof initialStepData>) => void;
|
patchStepData: (patch: Partial<typeof initialStepData>) => void;
|
||||||
|
|
||||||
|
spreadsheet: File | null;
|
||||||
|
setSpreadsheet: (spreadsheet: File | null) => void;
|
||||||
|
|
||||||
sheetId: string | null;
|
sheetId: string | null;
|
||||||
setSheetId: (sheetId: string | null) => void;
|
setSheetId: (sheetId: string | null) => void;
|
||||||
|
|
||||||
authenticationStatus: AuthenticationStatus;
|
authenticationStatus: AuthenticationStatus;
|
||||||
setAuthenticationStatus: (status: AuthenticationStatus) => void;
|
setAuthenticationStatus: (status: AuthenticationStatus) => void;
|
||||||
|
|
||||||
|
// we get this from a preview response
|
||||||
rundown: OntimeRundown | null;
|
rundown: OntimeRundown | null;
|
||||||
setRundown: (rundown: OntimeRundown | null) => void;
|
setRundown: (rundown: OntimeRundown | null) => void;
|
||||||
|
|
||||||
userFields: UserFields | null;
|
// we get this from a preview response
|
||||||
setUserFields: (userFields: UserFields | null) => void;
|
customFields: CustomFields | null;
|
||||||
|
setCustomFields: (customFields: CustomFields | null) => void;
|
||||||
|
|
||||||
worksheetOptions: string[] | null;
|
spreadsheetImportMap: ImportMap;
|
||||||
setWorksheetOptions: (worksheetOptions: string[] | null) => void;
|
patchSpreadsheetImportMap: <T extends keyof ImportMap>(field: T, value: ImportMap[T]) => void;
|
||||||
|
|
||||||
excelFileOptions: ExcelImportMap;
|
|
||||||
patchExcelFileOptions: <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => void;
|
|
||||||
|
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
resetPreview: () => void;
|
resetPreview: () => void;
|
||||||
@@ -39,12 +39,12 @@ const initialStepData = {
|
|||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
stepData: initialStepData,
|
stepData: initialStepData,
|
||||||
|
spreadsheet: null,
|
||||||
sheetId: null,
|
sheetId: null,
|
||||||
authenticationStatus: 'not_authenticated' as AuthenticationStatus,
|
authenticationStatus: 'not_authenticated' as AuthenticationStatus,
|
||||||
rundown: null,
|
rundown: null,
|
||||||
userFields: null,
|
customFields: null,
|
||||||
worksheetOptions: null,
|
spreadsheetImportMap: defaultImportMap,
|
||||||
excelFileOptions: defaultExcelImportMap,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSheetStore = create<SheetStore>((set, get) => ({
|
export const useSheetStore = create<SheetStore>((set, get) => ({
|
||||||
@@ -55,23 +55,23 @@ export const useSheetStore = create<SheetStore>((set, get) => ({
|
|||||||
set({ stepData: { ...stepData, ...patch } });
|
set({ stepData: { ...stepData, ...patch } });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setSpreadsheet: (spreadsheet: File | null) => set({ spreadsheet }),
|
||||||
|
|
||||||
setSheetId: (sheetId: string | null) => set({ sheetId }),
|
setSheetId: (sheetId: string | null) => set({ sheetId }),
|
||||||
|
|
||||||
setAuthenticationStatus: (status: AuthenticationStatus) => set({ authenticationStatus: status }),
|
setAuthenticationStatus: (status: AuthenticationStatus) => set({ authenticationStatus: status }),
|
||||||
|
|
||||||
setRundown: (rundown: OntimeRundown | null) => set({ rundown }),
|
setRundown: (rundown: OntimeRundown | null) => set({ rundown }),
|
||||||
|
|
||||||
setUserFields: (userFields: UserFields | null) => set({ userFields }),
|
setCustomFields: (customFields: CustomFields | null) => set({ customFields }),
|
||||||
|
|
||||||
setWorksheetOptions: (worksheetOptions: string[] | null) => set({ worksheetOptions }),
|
patchSpreadsheetImportMap: <T extends keyof ImportMap>(field: T, value: ImportMap[T]) => {
|
||||||
|
const currentImportMap = get().spreadsheetImportMap;
|
||||||
patchExcelFileOptions: <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => {
|
if (currentImportMap[field] !== value) {
|
||||||
const excelFileOptions = get().excelFileOptions;
|
currentImportMap[field] = value;
|
||||||
if (excelFileOptions[field] !== value) {
|
|
||||||
excelFileOptions[field] = value;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
reset: () => set(initialState),
|
reset: () => set(initialState),
|
||||||
resetPreview: () => set({ rundown: null, userFields: null }),
|
resetPreview: () => set({ rundown: null, customFields: null }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -13,30 +13,9 @@ export const defaultColumnOrder: OntimeEntryCommonKeys[] = [
|
|||||||
'subtitle',
|
'subtitle',
|
||||||
'presenter',
|
'presenter',
|
||||||
'note',
|
'note',
|
||||||
'user0',
|
|
||||||
'user1',
|
|
||||||
'user2',
|
|
||||||
'user3',
|
|
||||||
'user4',
|
|
||||||
'user5',
|
|
||||||
'user6',
|
|
||||||
'user7',
|
|
||||||
'user8',
|
|
||||||
'user9',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description set default hidden columns
|
* @description set default hidden columns
|
||||||
*/
|
*/
|
||||||
export const defaultHiddenColumns: (keyof OntimeEvent)[] = [
|
export const defaultHiddenColumns: (keyof OntimeEvent)[] = [];
|
||||||
'user0',
|
|
||||||
'user1',
|
|
||||||
'user2',
|
|
||||||
'user3',
|
|
||||||
'user4',
|
|
||||||
'user5',
|
|
||||||
'user6',
|
|
||||||
'user7',
|
|
||||||
'user8',
|
|
||||||
'user9',
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { ChangeEvent, useRef, useState } from 'react';
|
import { ChangeEvent, useRef, useState } from 'react';
|
||||||
import { Input } from '@chakra-ui/react';
|
import { Input } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { validateFile } from '../../../common/utils/uploadUtils';
|
||||||
|
|
||||||
import UploadEntry from './upload-entry/UploadEntry';
|
import UploadEntry from './upload-entry/UploadEntry';
|
||||||
import { useUploadModalContextStore } from './uploadModalContext';
|
import { useUploadModalContextStore } from './uploadModalContext';
|
||||||
import { validateFile } from './uploadUtils';
|
|
||||||
|
|
||||||
import style from './UploadModal.module.scss';
|
import style from './UploadModal.module.scss';
|
||||||
|
|
||||||
@@ -52,12 +53,12 @@ export default function UploadFile() {
|
|||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
type='file'
|
type='file'
|
||||||
onChange={handleFile}
|
onChange={handleFile}
|
||||||
accept='.json, .xlsx'
|
accept='.json'
|
||||||
data-testid='file-input'
|
data-testid='file-input'
|
||||||
/>
|
/>
|
||||||
{!file && (
|
{!file && (
|
||||||
<div className={style.uploadArea} onClick={handleClick} role='button'>
|
<div className={style.uploadArea} onClick={handleClick} role='button'>
|
||||||
Click to select Ontime project or xlsx rundown
|
Click to select Ontime project
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(file || errors) && <UploadEntry file={file} errors={errors} progress={progress} handleClear={clearFile} />}
|
{(file || errors) && <UploadEntry file={file} errors={errors} progress={progress} handleClear={clearFile} />}
|
||||||
|
|||||||
@@ -9,27 +9,15 @@ import {
|
|||||||
ModalHeader,
|
ModalHeader,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { OntimeRundown } from 'ontime-types';
|
||||||
import { OntimeRundown, UserFields } from 'ontime-types';
|
|
||||||
import { defaultExcelImportMap, ExcelImportMap } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { RUNDOWN, USERFIELDS } from '../../../common/api/apiConstants';
|
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/apiUtils';
|
import { invalidateAllCaches, maybeAxiosError } from '../../../common/api/apiUtils';
|
||||||
import {
|
import { ProjectFileImportOptions, uploadProjectFile } from '../../../common/api/ontimeApi';
|
||||||
patchData,
|
import { isOntimeFile } from '../../../common/utils/uploadUtils';
|
||||||
postPreviewExcel,
|
|
||||||
ProjectFileImportOptions,
|
|
||||||
uploadProjectFile,
|
|
||||||
} from '../../../common/api/ontimeApi';
|
|
||||||
import { userFieldsPlaceholder } from '../../../common/models/UserFields';
|
|
||||||
|
|
||||||
import PreviewExcel from './preview/PreviewExcel';
|
|
||||||
import ExcelFileOptions from './upload-options/ExcelFileOptions';
|
|
||||||
import OntimeFileOptions from './upload-options/OntimeFileOptions';
|
import OntimeFileOptions from './upload-options/OntimeFileOptions';
|
||||||
import UploadStepTracker from './upload-step/UploadStep';
|
|
||||||
import UploadFile from './UploadFile';
|
import UploadFile from './UploadFile';
|
||||||
import { useUploadModalContextStore } from './uploadModalContext';
|
import { useUploadModalContextStore } from './uploadModalContext';
|
||||||
import { getPersistedOptions, isExcelFile, isOntimeFile, persistOptions } from './uploadUtils';
|
|
||||||
|
|
||||||
import style from './UploadModal.module.scss';
|
import style from './UploadModal.module.scss';
|
||||||
|
|
||||||
@@ -41,19 +29,15 @@ interface UploadModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { file, setProgress, clear } = useUploadModalContextStore();
|
const { file, setProgress, clear } = useUploadModalContextStore();
|
||||||
|
|
||||||
const [uploadStep, setUploadStep] = useState<UploadStep>('import');
|
const [uploadStep, setUploadStep] = useState<UploadStep>('import');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [rundown, setRundown] = useState<OntimeRundown | null>(null);
|
const [rundown, setRundown] = useState<OntimeRundown | null>(null);
|
||||||
const [userFields, setUserFields] = useState<UserFields | null>(null);
|
|
||||||
|
|
||||||
const [errors, setErrors] = useState('');
|
const [errors, setErrors] = useState('');
|
||||||
|
|
||||||
const ontimeFileOptions = useRef<Partial<ProjectFileImportOptions>>({});
|
const ontimeFileOptions = useRef<Partial<ProjectFileImportOptions>>({});
|
||||||
const excelFileOptions = useRef<ExcelImportMap>(defaultExcelImportMap);
|
|
||||||
|
|
||||||
const updateOntimeFileOptions = <T extends keyof ProjectFileImportOptions>(
|
const updateOntimeFileOptions = <T extends keyof ProjectFileImportOptions>(
|
||||||
field: T,
|
field: T,
|
||||||
@@ -62,27 +46,12 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
|||||||
ontimeFileOptions.current = { ...ontimeFileOptions.current, [field]: value };
|
ontimeFileOptions.current = { ...ontimeFileOptions.current, [field]: value };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateExcelFileOptions = <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => {
|
|
||||||
if (excelFileOptions.current[field] !== value) {
|
|
||||||
excelFileOptions.current = { ...excelFileOptions.current, [field]: value };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// We want to populate the options with any previous options given by the user
|
|
||||||
useEffect(() => {
|
|
||||||
const excelOptions = getPersistedOptions('excel');
|
|
||||||
if (excelOptions) {
|
|
||||||
excelFileOptions.current = excelOptions;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// if the modal re-opens, we want to restart all states
|
// if the modal re-opens, we want to restart all states
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clear();
|
clear();
|
||||||
setUploadStep('import');
|
setUploadStep('import');
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
setRundown(null);
|
setRundown(null);
|
||||||
setUserFields(null);
|
|
||||||
setErrors('');
|
setErrors('');
|
||||||
}, [clear, isOpen]);
|
}, [clear, isOpen]);
|
||||||
|
|
||||||
@@ -102,10 +71,6 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
|||||||
await handleOntimeFile(file, options);
|
await handleOntimeFile(file, options);
|
||||||
await invalidateAllCaches();
|
await invalidateAllCaches();
|
||||||
doClose = true;
|
doClose = true;
|
||||||
} else if (isExcelFile(file)) {
|
|
||||||
const options = excelFileOptions.current;
|
|
||||||
persistOptions({ optionType: 'excel', options });
|
|
||||||
await handleExcelFile(file, options);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = maybeAxiosError(error);
|
const message = maybeAxiosError(error);
|
||||||
@@ -118,17 +83,6 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// when we upload excel, we populate state with preview data
|
|
||||||
async function handleExcelFile(file: File, options: ExcelImportMap) {
|
|
||||||
const response = await postPreviewExcel(file, setProgress, options);
|
|
||||||
if (response.status === 200) {
|
|
||||||
setRundown(response.data.rundown);
|
|
||||||
setUserFields(response.data.userFields);
|
|
||||||
// in excel imports we have an extra review step
|
|
||||||
setUploadStep('review');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// when we upload project files, no extra operations are done
|
// when we upload project files, no extra operations are done
|
||||||
async function handleOntimeFile(file: File, options: Partial<ProjectFileImportOptions>) {
|
async function handleOntimeFile(file: File, options: Partial<ProjectFileImportOptions>) {
|
||||||
await uploadProjectFile(file, setProgress, options);
|
await uploadProjectFile(file, setProgress, options);
|
||||||
@@ -139,37 +93,9 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
|||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
clear();
|
clear();
|
||||||
setRundown([]);
|
setRundown([]);
|
||||||
setUserFields(userFieldsPlaceholder);
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinalise = async () => {
|
|
||||||
// this step is currently only used for excel files, after preview
|
|
||||||
if (isExcel && rundown && userFields) {
|
|
||||||
let doClose = false;
|
|
||||||
setSubmitting(true);
|
|
||||||
try {
|
|
||||||
await patchData({ rundown, userFields });
|
|
||||||
// TODO: broken :(
|
|
||||||
// we need to normalise the data here
|
|
||||||
queryClient.setQueryData(RUNDOWN, { rundown, revision: -1 });
|
|
||||||
queryClient.setQueryData(USERFIELDS, userFields);
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: [...RUNDOWN, ...USERFIELDS],
|
|
||||||
});
|
|
||||||
doClose = true;
|
|
||||||
} catch (error) {
|
|
||||||
const message = maybeAxiosError(error);
|
|
||||||
setErrors(`Failed applying changes ${message}`);
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
if (doClose) {
|
|
||||||
handleClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const undoReview = () => {
|
const undoReview = () => {
|
||||||
setUploadStep('import');
|
setUploadStep('import');
|
||||||
setErrors('');
|
setErrors('');
|
||||||
@@ -177,11 +103,10 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
|||||||
|
|
||||||
const isImporting = uploadStep === 'import';
|
const isImporting = uploadStep === 'import';
|
||||||
const isReview = uploadStep === 'review';
|
const isReview = uploadStep === 'review';
|
||||||
const isExcel = isExcelFile(file);
|
|
||||||
const isOntime = isOntimeFile(file);
|
const isOntime = isOntimeFile(file);
|
||||||
|
|
||||||
const handleGoBack = isImporting ? undefined : undoReview;
|
const handleGoBack = isImporting ? undefined : undoReview;
|
||||||
const handleSubmit = isImporting ? handleUpload : handleFinalise;
|
const handleSubmit = handleUpload;
|
||||||
const disableSubmit = (isImporting && !file) || (isReview && rundown === null);
|
const disableSubmit = (isImporting && !file) || (isReview && rundown === null);
|
||||||
const disableGoBack = isImporting;
|
const disableGoBack = isImporting;
|
||||||
const submitText = isImporting ? 'Import' : 'Finish';
|
const submitText = isImporting ? 'Import' : 'Finish';
|
||||||
@@ -202,18 +127,10 @@ export default function UploadModal({ onClose, isOpen }: UploadModalProps) {
|
|||||||
<ModalHeader>File import</ModalHeader>
|
<ModalHeader>File import</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody className={style.uploadBody}>
|
<ModalBody className={style.uploadBody}>
|
||||||
{isExcel && <UploadStepTracker uploadStep={uploadStep} />}
|
<>
|
||||||
{uploadStep === 'import' ? (
|
<UploadFile />
|
||||||
<>
|
{isOntime && <OntimeFileOptions optionsRef={ontimeFileOptions} updateOptions={updateOntimeFileOptions} />}
|
||||||
<UploadFile />
|
</>
|
||||||
{isOntime && <OntimeFileOptions optionsRef={ontimeFileOptions} updateOptions={updateOntimeFileOptions} />}
|
|
||||||
{isExcel && (
|
|
||||||
<ExcelFileOptions importOptions={excelFileOptions.current} updateOptions={updateExcelFileOptions} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<PreviewExcel rundown={rundown ?? []} userFields={userFields ?? userFieldsPlaceholder} />
|
|
||||||
)}
|
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<div className={style.feedbackSection}>{errors && <div className={style.error}>{errors}</div>}</div>
|
<div className={style.feedbackSection}>{errors && <div className={style.error}>{errors}</div>}</div>
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { OntimeRundown, UserFields } from 'ontime-types';
|
|
||||||
|
|
||||||
import PreviewRundown from './PreviewRundown';
|
|
||||||
|
|
||||||
import style from '../../Modal.module.scss';
|
|
||||||
|
|
||||||
interface PreviewExcelProps {
|
|
||||||
rundown: OntimeRundown;
|
|
||||||
userFields: UserFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PreviewExcel(props: PreviewExcelProps) {
|
|
||||||
const { rundown, userFields } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${style.column}`}>
|
|
||||||
<div className={style.title}>Review Rundown</div>
|
|
||||||
<PreviewRundown rundown={rundown} userFields={userFields} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
import { Fragment } from 'react';
|
|
||||||
import { isOntimeBlock, isOntimeEvent, OntimeRundown, UserFields } from 'ontime-types';
|
|
||||||
import { millisToString } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { getAccessibleColour } from '../../../../common/utils/styleUtils';
|
|
||||||
|
|
||||||
import Tag from './Tag';
|
|
||||||
|
|
||||||
import style from './PreviewTable.module.scss';
|
|
||||||
|
|
||||||
interface PreviewRundownProps {
|
|
||||||
rundown: OntimeRundown;
|
|
||||||
userFields: UserFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
function booleanToText(value?: boolean) {
|
|
||||||
return value ? 'Yes' : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PreviewRundown({ rundown, userFields }: PreviewRundownProps) {
|
|
||||||
// we only count Ontime Events which are 1 based in client
|
|
||||||
let eventIndex = 0;
|
|
||||||
return (
|
|
||||||
<div className={style.container}>
|
|
||||||
<table className={style.rundownPreview}>
|
|
||||||
<thead className={style.header}>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Cue</th>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>Subtitle</th>
|
|
||||||
<th>Presenter</th>
|
|
||||||
<th>Time Start</th>
|
|
||||||
<th>Time End</th>
|
|
||||||
<th>Duration</th>
|
|
||||||
<th>Warning Time</th>
|
|
||||||
<th>Danger Time</th>
|
|
||||||
<th>Is Public</th>
|
|
||||||
<th>Skip</th>
|
|
||||||
<th>Colour</th>
|
|
||||||
<th>Timer Type</th>
|
|
||||||
<th>End Action</th>
|
|
||||||
<th>
|
|
||||||
user0 <Tag>{userFields.user0}</Tag>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
user1 <Tag>{userFields.user1}</Tag>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
user2 <Tag>{userFields.user2}</Tag>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
user3 <Tag>{userFields.user3}</Tag>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
user4 <Tag>{userFields.user4}</Tag>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
user5 <Tag>{userFields.user5}</Tag>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
user6 <Tag>{userFields.user6}</Tag>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
user7 <Tag>{userFields.user7}</Tag>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
user8 <Tag>{userFields.user8}</Tag>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
user9 <Tag>{userFields.user9}</Tag>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className={style.body}>
|
|
||||||
{rundown.map((event) => {
|
|
||||||
if (isOntimeBlock(event)) {
|
|
||||||
return (
|
|
||||||
<tr key={event.id}>
|
|
||||||
<td className={style.center}>
|
|
||||||
<Tag>-</Tag>
|
|
||||||
</td>
|
|
||||||
<td className={style.center}>
|
|
||||||
<Tag>{event.type}</Tag>
|
|
||||||
</td>
|
|
||||||
<td />
|
|
||||||
<td colSpan={99}>{event.title}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!isOntimeEvent(event)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
eventIndex += 1;
|
|
||||||
const colour = event.colour ? getAccessibleColour(event.colour) : {};
|
|
||||||
const isPublic = booleanToText(event.isPublic);
|
|
||||||
const skip = booleanToText(event.skip);
|
|
||||||
return (
|
|
||||||
<Fragment key={event.id}>
|
|
||||||
<tr>
|
|
||||||
<td className={style.center}>
|
|
||||||
<Tag>{eventIndex}</Tag>
|
|
||||||
</td>
|
|
||||||
<td className={style.center}>
|
|
||||||
<Tag>{event.type}</Tag>
|
|
||||||
</td>
|
|
||||||
<td className={style.nowrap}>{event.cue}</td>
|
|
||||||
<td>{event.title}</td>
|
|
||||||
<td>{event.subtitle}</td>
|
|
||||||
<td>{event.presenter}</td>
|
|
||||||
<td>{millisToString(event.timeStart)}</td>
|
|
||||||
<td>{millisToString(event.timeEnd)}</td>
|
|
||||||
<td>{millisToString(event.duration)}</td>
|
|
||||||
<td>{millisToString(event.timeWarning)}</td>
|
|
||||||
<td>{millisToString(event.timeDanger)}</td>
|
|
||||||
<td className={style.center}>{isPublic && <Tag>{isPublic}</Tag>}</td>
|
|
||||||
<td>{skip && <Tag>{skip}</Tag>}</td>
|
|
||||||
<td style={{ ...colour }}>{event.colour}</td>
|
|
||||||
<td className={style.center}>
|
|
||||||
<Tag>{event.timerType}</Tag>
|
|
||||||
</td>
|
|
||||||
<td className={style.center}>
|
|
||||||
<Tag>{event.endAction}</Tag>
|
|
||||||
</td>
|
|
||||||
<td>{event.user0}</td>
|
|
||||||
<td>{event.user1}</td>
|
|
||||||
<td>{event.user2}</td>
|
|
||||||
<td>{event.user3}</td>
|
|
||||||
<td>{event.user4}</td>
|
|
||||||
<td>{event.user5}</td>
|
|
||||||
<td>{event.user6}</td>
|
|
||||||
<td>{event.user7}</td>
|
|
||||||
<td>{event.user8}</td>
|
|
||||||
<td>{event.user9}</td>
|
|
||||||
</tr>
|
|
||||||
{event.note && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={99} className={style.secondaryRow}>
|
|
||||||
Note: {event.note}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
.container {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: max(300px, 30vh);
|
|
||||||
overflow: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rundownPreview {
|
|
||||||
font-size: calc(1rem - 2px);
|
|
||||||
overflow-x: scroll;
|
|
||||||
tr td:first-child,
|
|
||||||
tr th:first-child {
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
z-index: 2;
|
|
||||||
background-color: white;
|
|
||||||
box-shadow: 1px 0 $gray-50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header,
|
|
||||||
.body {
|
|
||||||
th {
|
|
||||||
font-weight: 400;
|
|
||||||
height: unset;
|
|
||||||
line-height: calc(1rem - 2px);
|
|
||||||
white-space: nowrap;
|
|
||||||
padding-left: 0.25rem;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
background-color: white;
|
|
||||||
box-shadow: 0 2px $gray-50;
|
|
||||||
z-index: 3;
|
|
||||||
|
|
||||||
th {
|
|
||||||
font-weight: 200;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr {
|
|
||||||
word-wrap: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.body {
|
|
||||||
td {
|
|
||||||
text-align: left;
|
|
||||||
vertical-align: top;
|
|
||||||
padding: 0 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nowrap {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondaryRow {
|
|
||||||
padding: 0.25em 0.25em;
|
|
||||||
background-color: $gray-50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ import { IoClose } from '@react-icons/all-files/io5/IoClose';
|
|||||||
import { IoDocumentTextOutline } from '@react-icons/all-files/io5/IoDocumentTextOutline';
|
import { IoDocumentTextOutline } from '@react-icons/all-files/io5/IoDocumentTextOutline';
|
||||||
import { IoWarningOutline } from '@react-icons/all-files/io5/IoWarningOutline';
|
import { IoWarningOutline } from '@react-icons/all-files/io5/IoWarningOutline';
|
||||||
|
|
||||||
import { isExcelFile, isOntimeFile } from '../uploadUtils';
|
import { isOntimeFile } from '../../../../common/utils/uploadUtils';
|
||||||
|
|
||||||
import style from './UploadEntry.module.scss';
|
import style from './UploadEntry.module.scss';
|
||||||
|
|
||||||
@@ -33,8 +33,6 @@ export default function UploadEntry(props: UploadEntryProps) {
|
|||||||
let fileType = '';
|
let fileType = '';
|
||||||
if (isOntimeFile(file)) {
|
if (isOntimeFile(file)) {
|
||||||
fileType = 'Ontime Project File';
|
fileType = 'Ontime Project File';
|
||||||
} else if (isExcelFile(file)) {
|
|
||||||
fileType = 'Excel Rundown';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { ExcelImportMap } from 'ontime-utils';
|
|
||||||
|
|
||||||
import ImportMapTable, { type TableEntry } from './ImportMapTable';
|
|
||||||
|
|
||||||
import style from '../UploadModal.module.scss';
|
|
||||||
|
|
||||||
interface ExcelFileOptionsProps {
|
|
||||||
importOptions: ExcelImportMap;
|
|
||||||
updateOptions: <T extends keyof ExcelImportMap>(field: T, value: ExcelImportMap[T]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ExcelFileOptions(props: ExcelFileOptionsProps) {
|
|
||||||
const { importOptions, updateOptions } = props;
|
|
||||||
|
|
||||||
const worksheet: TableEntry[] = [{ label: 'Worksheet', title: 'worksheet', value: importOptions.worksheet }];
|
|
||||||
|
|
||||||
const timings: TableEntry[] = [
|
|
||||||
{ label: 'Start time', title: 'timeStart', value: importOptions.timeStart },
|
|
||||||
{ label: 'End Time', title: 'timeEnd', value: importOptions.timeEnd },
|
|
||||||
{ label: 'Duration', title: 'duration', value: importOptions.duration },
|
|
||||||
{ label: 'Warning Time', title: 'timeWarning', value: importOptions.timeWarning },
|
|
||||||
{ label: 'Danger Time', title: 'timeDanger', value: importOptions.timeDanger },
|
|
||||||
];
|
|
||||||
|
|
||||||
const titles: TableEntry[] = [
|
|
||||||
{ label: 'Cue', title: 'cue', value: importOptions.cue },
|
|
||||||
{ label: 'Colour', title: 'colour', value: importOptions.colour },
|
|
||||||
{ label: 'Title', title: 'title', value: importOptions.title },
|
|
||||||
{ label: 'Presenter', title: 'presenter', value: importOptions.presenter },
|
|
||||||
{ label: 'Subtitle', title: 'subtitle', value: importOptions.subtitle },
|
|
||||||
{ label: 'Note', title: 'note', value: importOptions.note },
|
|
||||||
];
|
|
||||||
|
|
||||||
const options: TableEntry[] = [
|
|
||||||
{ label: 'Is Public', title: 'isPublic', value: importOptions.isPublic },
|
|
||||||
{ label: 'Skip', title: 'skip', value: importOptions.skip },
|
|
||||||
{ label: 'Timer Type', title: 'timerType', value: importOptions.timerType },
|
|
||||||
{ label: 'End Action', title: 'endAction', value: importOptions.endAction },
|
|
||||||
];
|
|
||||||
|
|
||||||
const userFields: TableEntry[] = [
|
|
||||||
{ label: 'User 0', title: 'user0', value: importOptions.user0 },
|
|
||||||
{ label: 'User 1', title: 'user1', value: importOptions.user1 },
|
|
||||||
{ label: 'User 2', title: 'user2', value: importOptions.user2 },
|
|
||||||
{ label: 'User 3', title: 'user3', value: importOptions.user3 },
|
|
||||||
{ label: 'User 4', title: 'user4', value: importOptions.user4 },
|
|
||||||
{ label: 'User 5', title: 'user5', value: importOptions.user5 },
|
|
||||||
{ label: 'User 6', title: 'user6', value: importOptions.user6 },
|
|
||||||
{ label: 'User 7', title: 'user7', value: importOptions.user7 },
|
|
||||||
{ label: 'User 8', title: 'user8', value: importOptions.user8 },
|
|
||||||
{ label: 'User 9', title: 'user9', value: importOptions.user9 },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.uploadOptions}>
|
|
||||||
<div className={style.twoEqualColumn}>
|
|
||||||
<ImportMapTable title='Sheet settings' fields={worksheet} handleOnChange={updateOptions} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={style.twoEqualColumn}>
|
|
||||||
<ImportMapTable title='Timings' fields={timings} handleOnChange={updateOptions} />
|
|
||||||
<ImportMapTable title='Options' fields={options} handleOnChange={updateOptions} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={style.twoEqualColumn}>
|
|
||||||
<ImportMapTable title='Titles' fields={titles} handleOnChange={updateOptions} />
|
|
||||||
<ImportMapTable title='User Fields' fields={userFields} handleOnChange={updateOptions} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
-30
@@ -1,30 +0,0 @@
|
|||||||
.importTable {
|
|
||||||
margin: 0.5rem;
|
|
||||||
height: fit-content;
|
|
||||||
|
|
||||||
thead {
|
|
||||||
color: $gray-500;
|
|
||||||
text-transform: uppercase;
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr:hover {
|
|
||||||
background-color: $gray-50;
|
|
||||||
}
|
|
||||||
|
|
||||||
tbody {
|
|
||||||
td {
|
|
||||||
max-width: fit-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 6em;
|
|
||||||
font-size: $inner-section-text-size;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { Input } from '@chakra-ui/react';
|
|
||||||
import { ExcelImportMap } from 'ontime-utils';
|
|
||||||
|
|
||||||
import style from './ImportMapTable.module.scss';
|
|
||||||
|
|
||||||
export type TableEntry = { label: string; title: keyof ExcelImportMap; value: string };
|
|
||||||
|
|
||||||
interface ImportMapTableProps {
|
|
||||||
title: string;
|
|
||||||
fields: TableEntry[];
|
|
||||||
handleOnChange: (field: keyof ExcelImportMap, value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ImportMapTable(props: ImportMapTableProps) {
|
|
||||||
const { title, fields, handleOnChange } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<table className={style.importTable}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td colSpan={2}>{title}</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{fields.map((field) => {
|
|
||||||
return (
|
|
||||||
<tr key={field.title}>
|
|
||||||
<td className={style.label}>
|
|
||||||
<label htmlFor={field.title}>{field.label}</label>
|
|
||||||
</td>
|
|
||||||
<td className={style.input}>
|
|
||||||
<Input
|
|
||||||
id={field.title}
|
|
||||||
size='sm'
|
|
||||||
variant='ontime-filled'
|
|
||||||
maxLength={25}
|
|
||||||
defaultValue={field.value}
|
|
||||||
placeholder='Use default column name'
|
|
||||||
onBlur={(event) => {
|
|
||||||
handleOnChange(field.title, event.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { CustomFields, isOntimeEvent, OntimeEvent, SupportedEvent, UserFields } from 'ontime-types';
|
import { CustomField, CustomFields, isOntimeEvent, OntimeEvent, SupportedEvent } from 'ontime-types';
|
||||||
import { getFirstEventNormal, getLastEventNormal } from 'ontime-utils';
|
import { getFirstEventNormal, getLastEventNormal } from 'ontime-utils';
|
||||||
|
|
||||||
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../common/components/navigation-menu/NavigationMenu';
|
||||||
@@ -107,7 +107,7 @@ export default function Operator() {
|
|||||||
|
|
||||||
const handleEdit = useCallback(
|
const handleEdit = useCallback(
|
||||||
(event: EditEvent) => {
|
(event: EditEvent) => {
|
||||||
const field = searchParams.get('subscribe') as keyof UserFields | null;
|
const field = searchParams.get('subscribe') as keyof CustomField | null;
|
||||||
|
|
||||||
if (field) {
|
if (field) {
|
||||||
setEditEvent({ ...event, field });
|
setEditEvent({ ...event, field });
|
||||||
|
|||||||
@@ -7,10 +7,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
|
padding-bottom: 4rem;
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -44,6 +46,7 @@
|
|||||||
font-size: calc(1rem - 3px);
|
font-size: calc(1rem - 3px);
|
||||||
color: $label-gray;
|
color: $label-gray;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delayLabel {
|
.delayLabel {
|
||||||
|
|||||||
@@ -16,25 +16,7 @@ import style from './EventEditor.module.scss';
|
|||||||
|
|
||||||
export type EventEditorSubmitActions = keyof OntimeEvent;
|
export type EventEditorSubmitActions = keyof OntimeEvent;
|
||||||
|
|
||||||
// TODO: this logic will become dynamic
|
export type EditorUpdateFields = 'cue' | 'title' | 'presenter' | 'subtitle' | 'note' | 'colour' | CustomFieldLabel; // TODO: keyof customFields
|
||||||
export type EditorUpdateFields =
|
|
||||||
| 'cue'
|
|
||||||
| 'title'
|
|
||||||
| 'presenter'
|
|
||||||
| 'subtitle'
|
|
||||||
| 'note'
|
|
||||||
| 'colour'
|
|
||||||
| 'user0'
|
|
||||||
| 'user1'
|
|
||||||
| 'user2'
|
|
||||||
| 'user3'
|
|
||||||
| 'user4'
|
|
||||||
| 'user5'
|
|
||||||
| 'user6'
|
|
||||||
| 'user7'
|
|
||||||
| 'user8'
|
|
||||||
| 'user9'
|
|
||||||
| CustomFieldLabel; // TODO: keyof customFields
|
|
||||||
|
|
||||||
export default function EventEditor() {
|
export default function EventEditor() {
|
||||||
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
const selectedEvents = useEventSelection((state) => state.selectedEvents);
|
||||||
@@ -85,8 +67,6 @@ export default function EventEditor() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customKeys = Object.keys(customFields ?? {});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.eventEditor} data-testid='editor-container'>
|
<div className={style.eventEditor} data-testid='editor-container'>
|
||||||
<div className={style.content}>
|
<div className={style.content}>
|
||||||
@@ -123,13 +103,17 @@ export default function EventEditor() {
|
|||||||
Manage
|
Manage
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{customKeys.map((label) => {
|
{Object.keys(customFields).map((label) => {
|
||||||
|
const key = `${event.id}-${label}`;
|
||||||
|
const fieldName = `custom-${label}`;
|
||||||
|
const initialValue = event.custom[label]?.value ?? '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EventTextArea
|
<EventTextArea
|
||||||
key={`${event.id}-${label}`}
|
key={key}
|
||||||
field={`custom-${label}`}
|
field={fieldName}
|
||||||
label={label}
|
label={label}
|
||||||
initialValue={event.custom[label]?.value ?? ''}
|
initialValue={initialValue}
|
||||||
submitHandler={handleSubmit}
|
submitHandler={handleSubmit}
|
||||||
className={style.decorated}
|
className={style.decorated}
|
||||||
style={{ '--decorator-color': customFields[label].colour } as CSSProperties}
|
style={{ '--decorator-color': customFields[label].colour } as CSSProperties}
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ const EventEditorTitles = (props: EventEditorLeftProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<EventTextInput field='cue' label='Cue' initialValue={cue} submitHandler={cueSubmitHandler} maxLength={10} />
|
<EventTextInput field='cue' label='Cue' initialValue={cue} submitHandler={cueSubmitHandler} maxLength={10} />
|
||||||
</div>
|
</div>
|
||||||
<EventTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
|
|
||||||
<EventTextInput field='presenter' label='Presenter' initialValue={presenter} submitHandler={handleSubmit} />
|
|
||||||
<EventTextInput field='subtitle' label='Subtitle' initialValue={subtitle} submitHandler={handleSubmit} />
|
|
||||||
<div>
|
<div>
|
||||||
<label className={style.inputLabel}>Colour</label>
|
<label className={style.inputLabel}>Colour</label>
|
||||||
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
|
<SwatchSelect name='colour' value={colour} handleChange={handleSubmit} />
|
||||||
</div>
|
</div>
|
||||||
|
<EventTextInput field='title' label='Title' initialValue={title} submitHandler={handleSubmit} />
|
||||||
|
<EventTextInput field='presenter' label='Presenter' initialValue={presenter} submitHandler={handleSubmit} />
|
||||||
|
<EventTextInput field='subtitle' label='Subtitle' initialValue={subtitle} submitHandler={handleSubmit} />
|
||||||
<EventTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
|
<EventTextArea field='note' label='Note' initialValue={note} submitHandler={handleSubmit} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -153,11 +153,12 @@ export const startServer = async () => {
|
|||||||
checkStart(OntimeStartOrder.InitServer);
|
checkStart(OntimeStartOrder.InitServer);
|
||||||
|
|
||||||
const { serverPort } = DataProvider.getSettings();
|
const { serverPort } = DataProvider.getSettings();
|
||||||
|
|
||||||
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
const returnMessage = `Ontime is listening on port ${serverPort}`;
|
||||||
|
|
||||||
expressServer = http.createServer(app);
|
expressServer = http.createServer(app);
|
||||||
|
|
||||||
socket.init(expressServer);
|
socket.init(expressServer);
|
||||||
|
logger.info(LogOrigin.Server, returnMessage);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module initialises the services and provides initial payload for the store
|
* Module initialises the services and provides initial payload for the store
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
ViewSettings,
|
ViewSettings,
|
||||||
DatabaseModel,
|
DatabaseModel,
|
||||||
OSCSettings,
|
OSCSettings,
|
||||||
UserFields,
|
|
||||||
Alias,
|
Alias,
|
||||||
Settings,
|
Settings,
|
||||||
CustomFields,
|
CustomFields,
|
||||||
@@ -75,10 +74,6 @@ export class DataProvider {
|
|||||||
await this.persist();
|
await this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
static getUserFields() {
|
|
||||||
return { ...data.userFields };
|
|
||||||
}
|
|
||||||
|
|
||||||
static getViewSettings() {
|
static getViewSettings() {
|
||||||
return { ...data.viewSettings };
|
return { ...data.viewSettings };
|
||||||
}
|
}
|
||||||
@@ -88,11 +83,6 @@ export class DataProvider {
|
|||||||
await this.persist();
|
await this.persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
static async setUserFields(newData: UserFields) {
|
|
||||||
data.userFields = { ...newData };
|
|
||||||
await this.persist();
|
|
||||||
}
|
|
||||||
|
|
||||||
static async setOsc(newData: OSCSettings): Promise<OSCSettings> {
|
static async setOsc(newData: OSCSettings): Promise<OSCSettings> {
|
||||||
data.osc = { ...newData };
|
data.osc = { ...newData };
|
||||||
await this.persist();
|
await this.persist();
|
||||||
@@ -124,7 +114,6 @@ export class DataProvider {
|
|||||||
data.osc = mergedData.osc;
|
data.osc = mergedData.osc;
|
||||||
data.http = mergedData.http;
|
data.http = mergedData.http;
|
||||||
data.aliases = mergedData.aliases;
|
data.aliases = mergedData.aliases;
|
||||||
data.userFields = mergedData.userFields;
|
|
||||||
data.customFields = mergedData.customFields;
|
data.customFields = mergedData.customFields;
|
||||||
data.rundown = mergedData.rundown;
|
data.rundown = mergedData.rundown;
|
||||||
await this.persist();
|
await this.persist();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { DatabaseModel } from 'ontime-types';
|
|||||||
* @param {object} newData
|
* @param {object} newData
|
||||||
*/
|
*/
|
||||||
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>) {
|
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>) {
|
||||||
const { rundown, project, settings, viewSettings, aliases, customFields, userFields, osc, http } = newData || {};
|
const { rundown, project, settings, viewSettings, aliases, customFields, osc, http } = newData || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...existing,
|
...existing,
|
||||||
@@ -16,10 +16,6 @@ export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseMode
|
|||||||
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
||||||
aliases: aliases ?? existing.aliases,
|
aliases: aliases ?? existing.aliases,
|
||||||
customFields: customFields ?? existing.customFields,
|
customFields: customFields ?? existing.customFields,
|
||||||
userFields: {
|
|
||||||
...existing.userFields,
|
|
||||||
...(userFields && Object.fromEntries(Object.entries(userFields).filter(([_, value]) => value !== null))),
|
|
||||||
},
|
|
||||||
osc: { ...existing.osc, ...osc },
|
osc: { ...existing.osc, ...osc },
|
||||||
http: { ...existing.http, ...http },
|
http: { ...existing.http, ...http },
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -29,18 +29,6 @@ describe('safeMerge', () => {
|
|||||||
dangerColor: '#ED3333',
|
dangerColor: '#ED3333',
|
||||||
},
|
},
|
||||||
aliases: [],
|
aliases: [],
|
||||||
userFields: {
|
|
||||||
user0: 'existing user0',
|
|
||||||
user1: 'existing user1',
|
|
||||||
user2: 'existing user2',
|
|
||||||
user3: 'existing user3',
|
|
||||||
user4: 'existing user4',
|
|
||||||
user5: 'existing user5',
|
|
||||||
user6: 'existing user6',
|
|
||||||
user7: 'existing user7',
|
|
||||||
user8: 'existing user8',
|
|
||||||
user9: 'existing user9',
|
|
||||||
},
|
|
||||||
customFields: {
|
customFields: {
|
||||||
lighting: { type: 'string', label: 'lighting', colour: 'red' },
|
lighting: { type: 'string', label: 'lighting', colour: 'red' },
|
||||||
vfx: { type: 'string', label: 'vfx', colour: 'blue' },
|
vfx: { type: 'string', label: 'vfx', colour: 'blue' },
|
||||||
@@ -166,18 +154,6 @@ describe('safeMerge', () => {
|
|||||||
endMessage: '',
|
endMessage: '',
|
||||||
},
|
},
|
||||||
aliases: [],
|
aliases: [],
|
||||||
userFields: {
|
|
||||||
user0: 'user0',
|
|
||||||
user1: 'user1',
|
|
||||||
user2: 'user2',
|
|
||||||
user3: 'user3',
|
|
||||||
user4: 'user4',
|
|
||||||
user5: 'user5',
|
|
||||||
user6: 'user6',
|
|
||||||
user7: 'user7',
|
|
||||||
user8: 'user8',
|
|
||||||
user9: 'user9',
|
|
||||||
},
|
|
||||||
osc: {
|
osc: {
|
||||||
portIn: 8888,
|
portIn: 8888,
|
||||||
portOut: 9999,
|
portOut: 9999,
|
||||||
@@ -200,34 +176,6 @@ describe('safeMerge', () => {
|
|||||||
expect(mergedData.aliases).toEqual(newData.aliases);
|
expect(mergedData.aliases).toEqual(newData.aliases);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('merges userFields into existing object', () => {
|
|
||||||
const existing = {
|
|
||||||
userFields: {
|
|
||||||
user0: 'Alice',
|
|
||||||
user1: 'Bob',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const newData = {
|
|
||||||
userFields: {
|
|
||||||
user2: 'Charlie',
|
|
||||||
user3: 'David',
|
|
||||||
user4: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
user0: 'Alice',
|
|
||||||
user1: 'Bob',
|
|
||||||
user2: 'Charlie',
|
|
||||||
user3: 'David',
|
|
||||||
};
|
|
||||||
|
|
||||||
//@ts-expect-error -- testing partial merge
|
|
||||||
const result = safeMerge(existing, newData);
|
|
||||||
expect(result.userFields).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('merges customFields into existing object', () => {
|
it('merges customFields into existing object', () => {
|
||||||
const existing = {
|
const existing = {
|
||||||
customFields: {
|
customFields: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { editEvent } from '../services/rundown-service/RundownService.js';
|
|||||||
import { getEventWithId } from '../services/rundown-service/rundownUtils.js';
|
import { getEventWithId } from '../services/rundown-service/rundownUtils.js';
|
||||||
import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../utils/coerceType.js';
|
import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../utils/coerceType.js';
|
||||||
|
|
||||||
|
// TODO: handle custom fields
|
||||||
const whitelistedPayload = {
|
const whitelistedPayload = {
|
||||||
title: coerceString,
|
title: coerceString,
|
||||||
subtitle: coerceString,
|
subtitle: coerceString,
|
||||||
@@ -16,17 +17,6 @@ const whitelistedPayload = {
|
|||||||
skip: coerceBoolean,
|
skip: coerceBoolean,
|
||||||
|
|
||||||
colour: coerceColour,
|
colour: coerceColour,
|
||||||
|
|
||||||
user0: coerceString,
|
|
||||||
user1: coerceString,
|
|
||||||
user2: coerceString,
|
|
||||||
user3: coerceString,
|
|
||||||
user4: coerceString,
|
|
||||||
user5: coerceString,
|
|
||||||
user6: coerceString,
|
|
||||||
user7: coerceString,
|
|
||||||
user8: coerceString,
|
|
||||||
user9: coerceString,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parse(property: string, value: unknown) {
|
export function parse(property: string, value: unknown) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
ProjectFileListResponse,
|
ProjectFileListResponse,
|
||||||
OSCSettings,
|
OSCSettings,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { ExcelImportOptions, deepmerge } from 'ontime-utils';
|
import { ImportOptions } from 'ontime-utils';
|
||||||
|
|
||||||
import { RequestHandler, Request, Response } from 'express';
|
import { RequestHandler, Request, Response } from 'express';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
@@ -72,16 +72,16 @@ export const dbDownload = async (_req: Request, res: Response) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a file and returns the result objects
|
* Parses a file and returns the result objects
|
||||||
* @param file
|
* @param filePath
|
||||||
* @param _req
|
* @param _req
|
||||||
* @param _res
|
* @param _res
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
async function parseFile(file, _req: Request, _res: Response, options: ExcelImportOptions) {
|
async function parseFile(filePath: string, _req: Request, _res: Response, options: ImportOptions) {
|
||||||
if (!fs.existsSync(file)) {
|
if (!fs.existsSync(filePath)) {
|
||||||
throw new Error('Upload failed');
|
throw new Error('Upload failed');
|
||||||
}
|
}
|
||||||
const result = await fileHandler(file, options);
|
const result = await fileHandler(filePath, options);
|
||||||
return result.data;
|
return result.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,29 +186,6 @@ export const postAliases = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/userfields'
|
|
||||||
// Returns -
|
|
||||||
export const getUserFields = async (_req: Request, res: Response) => {
|
|
||||||
const userFields = DataProvider.getUserFields();
|
|
||||||
res.status(200).send(userFields);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/ontime/userfields'
|
|
||||||
// Returns ACK message
|
|
||||||
export const postUserFields = async (req: Request, res: Response) => {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const persistedData = DataProvider.getUserFields();
|
|
||||||
const newData = deepmerge(persistedData, req.body);
|
|
||||||
await DataProvider.setUserFields(newData);
|
|
||||||
res.status(200).send(newData);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/ontime/settings'
|
// Create controller for POST request to '/ontime/settings'
|
||||||
// Returns -
|
// Returns -
|
||||||
export const getSettings = async (_req: Request, res: Response) => {
|
export const getSettings = async (_req: Request, res: Response) => {
|
||||||
@@ -356,7 +333,9 @@ export const postHTTP = async (req: Request, res: Response<HttpSettings | Ontime
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function patchPartialProjectFile(req: Request, res: Response) {
|
export async function patchPartialProjectFile(req: Request, res: Response) {
|
||||||
|
// all fields are optional in validation
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
|
res.status(400).send({ message: 'No field found to patch' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +346,7 @@ export async function patchPartialProjectFile(req: Request, res: Response) {
|
|||||||
viewSettings: req.body?.viewSettings,
|
viewSettings: req.body?.viewSettings,
|
||||||
osc: req.body?.osc,
|
osc: req.body?.osc,
|
||||||
aliases: req.body?.aliases,
|
aliases: req.body?.aliases,
|
||||||
userFields: req.body?.userFields,
|
customFields: req.body?.customFields,
|
||||||
};
|
};
|
||||||
|
|
||||||
const maybeRundown = req.body?.rundown;
|
const maybeRundown = req.body?.rundown;
|
||||||
@@ -402,10 +381,10 @@ export const dbUpload = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* uploads and parses an excel file
|
* uploads and parses an excel spreadsheet
|
||||||
* @returns parsed result
|
* @returns parsed result
|
||||||
*/
|
*/
|
||||||
export async function previewExcel(req: Request, res: Response) {
|
export async function previewSpreadsheet(req: Request, res: Response) {
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
res.status(400).send({ message: 'File not found' });
|
res.status(400).send({ message: 'File not found' });
|
||||||
return;
|
return;
|
||||||
@@ -413,8 +392,8 @@ export async function previewExcel(req: Request, res: Response) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const options = JSON.parse(req.body.options);
|
const options = JSON.parse(req.body.options);
|
||||||
const file = req.file.path;
|
const filePath = req.file.path;
|
||||||
const data = await parseFile(file, req, res, options);
|
const data = await parseFile(filePath, req, res, options);
|
||||||
res.status(200).send(data);
|
res.status(200).send(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).send({ message: String(error) });
|
res.status(500).send({ message: String(error) });
|
||||||
|
|||||||
@@ -39,28 +39,6 @@ export const validateAliases = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates object for POST /ontime/userfields
|
|
||||||
*/
|
|
||||||
export const validateUserFields = [
|
|
||||||
body('user0').exists().isString().trim(),
|
|
||||||
body('user1').exists().isString().trim(),
|
|
||||||
body('user2').exists().isString().trim(),
|
|
||||||
body('user3').exists().isString().trim(),
|
|
||||||
body('user4').exists().isString().trim(),
|
|
||||||
body('user5').exists().isString().trim(),
|
|
||||||
body('user6').exists().isString().trim(),
|
|
||||||
body('user7').exists().isString().trim(),
|
|
||||||
body('user8').exists().isString().trim(),
|
|
||||||
body('user9').exists().isString().trim(),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Validates object for POST /ontime/settings
|
* @description Validates object for POST /ontime/settings
|
||||||
*/
|
*/
|
||||||
@@ -122,7 +100,7 @@ export const validatePatchProjectFile = [
|
|||||||
body('settings').isObject().optional({ nullable: false }),
|
body('settings').isObject().optional({ nullable: false }),
|
||||||
body('viewSettings').isObject().optional({ nullable: false }),
|
body('viewSettings').isObject().optional({ nullable: false }),
|
||||||
body('aliases').isArray().optional({ nullable: false }),
|
body('aliases').isArray().optional({ nullable: false }),
|
||||||
body('userFields').isObject().optional({ nullable: false }),
|
body('customFields').isObject().optional({ nullable: false }),
|
||||||
body('osc').isObject().optional({ nullable: false }),
|
body('osc').isObject().optional({ nullable: false }),
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defaultExcelImportMap } from 'ontime-utils';
|
import { isImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { body, param, validationResult } from 'express-validator';
|
import { body, param, validationResult } from 'express-validator';
|
||||||
import { NextFunction, Request, Response } from 'express';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
@@ -26,17 +26,8 @@ export const validateSheetOptions = [
|
|||||||
.exists()
|
.exists()
|
||||||
.isObject()
|
.isObject()
|
||||||
.custom((content) => {
|
.custom((content) => {
|
||||||
// Check if the fileContent has the same keys as defaultExcelImportMap
|
const isValid = isImportMap(content);
|
||||||
const hasValidKeys = Object.keys(defaultExcelImportMap).every((key) => key in content);
|
return isValid;
|
||||||
|
|
||||||
// Check if all values in fileContent are strings
|
|
||||||
const hasValidValues = Object.values(content).every((value) => typeof value === 'string');
|
|
||||||
|
|
||||||
if (!hasValidKeys || !hasValidValues) {
|
|
||||||
throw new Error('Invalid file format');
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|||||||
@@ -2,18 +2,16 @@ import { initAssets, startIntegrations, startOSCServer, startServer } from './ap
|
|||||||
|
|
||||||
async function startOntime() {
|
async function startOntime() {
|
||||||
try {
|
try {
|
||||||
console.log('Starting Ontime');
|
console.log('Request: Initialise assets...');
|
||||||
console.log('Loading Assets');
|
|
||||||
await initAssets();
|
await initAssets();
|
||||||
console.log('Starting Server');
|
console.log('Request: Start server...');
|
||||||
await startServer();
|
await startServer();
|
||||||
console.log('Starting OSC Server');
|
console.log('Request: Start OSC server...');
|
||||||
await startOSCServer();
|
await startOSCServer();
|
||||||
console.log('Starting Integrations');
|
console.log('Request: Start OSC integrations...');
|
||||||
await startIntegrations();
|
await startIntegrations();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error starting Ontime');
|
console.log(`Request failed: ${error}`);
|
||||||
console.log(error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,18 +29,6 @@ export const dbModel: DatabaseModel = {
|
|||||||
},
|
},
|
||||||
aliases: [],
|
aliases: [],
|
||||||
customFields: {},
|
customFields: {},
|
||||||
userFields: {
|
|
||||||
user0: 'user0',
|
|
||||||
user1: 'user1',
|
|
||||||
user2: 'user2',
|
|
||||||
user3: 'user3',
|
|
||||||
user4: 'user4',
|
|
||||||
user5: 'user5',
|
|
||||||
user6: 'user6',
|
|
||||||
user7: 'user7',
|
|
||||||
user8: 'user8',
|
|
||||||
user9: 'user9',
|
|
||||||
},
|
|
||||||
osc: {
|
osc: {
|
||||||
portIn: 8888,
|
portIn: 8888,
|
||||||
portOut: 9999,
|
portOut: 9999,
|
||||||
|
|||||||
@@ -23,16 +23,6 @@ export const event: Omit<OntimeEvent, 'id' | 'delay' | 'cue'> = {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ import {
|
|||||||
getOSC,
|
getOSC,
|
||||||
getHTTP,
|
getHTTP,
|
||||||
getSettings,
|
getSettings,
|
||||||
getUserFields,
|
|
||||||
getViewSettings,
|
getViewSettings,
|
||||||
patchPartialProjectFile,
|
patchPartialProjectFile,
|
||||||
poll,
|
poll,
|
||||||
postAliases,
|
postAliases,
|
||||||
postOSC,
|
postOSC,
|
||||||
postSettings,
|
postSettings,
|
||||||
postUserFields,
|
|
||||||
postViewSettings,
|
postViewSettings,
|
||||||
previewExcel,
|
previewSpreadsheet,
|
||||||
postHTTP,
|
postHTTP,
|
||||||
duplicateProjectFile,
|
duplicateProjectFile,
|
||||||
listProjects,
|
listProjects,
|
||||||
@@ -32,7 +30,6 @@ import {
|
|||||||
validateOSC,
|
validateOSC,
|
||||||
validatePatchProjectFile,
|
validatePatchProjectFile,
|
||||||
validateSettings,
|
validateSettings,
|
||||||
validateUserFields,
|
|
||||||
viewValidator,
|
viewValidator,
|
||||||
validateHTTP,
|
validateHTTP,
|
||||||
validateProjectDuplicate,
|
validateProjectDuplicate,
|
||||||
@@ -61,11 +58,12 @@ router.get('/db', dbDownload);
|
|||||||
// create route between controller and '/ontime/db' endpoint
|
// create route between controller and '/ontime/db' endpoint
|
||||||
router.post('/db', uploadFile, dbUpload);
|
router.post('/db', uploadFile, dbUpload);
|
||||||
|
|
||||||
// create route between controller and '/ontime/excel' endpoint
|
// create route between controller and '/ontime/db' endpoint
|
||||||
router.patch('/db', validatePatchProjectFile, patchPartialProjectFile);
|
router.patch('/db', validatePatchProjectFile, patchPartialProjectFile);
|
||||||
|
|
||||||
// create route between controller and '/ontime/preview-spreadsheet' endpoint
|
// create route between controller and '/spreadsheet/preview' endpoint
|
||||||
router.post('/preview-spreadsheet', uploadFile, previewExcel);
|
// TODO: validate import map
|
||||||
|
router.post('/spreadsheet/preview', uploadFile, previewSpreadsheet);
|
||||||
|
|
||||||
// create route between controller and '/ontime/settings' endpoint
|
// create route between controller and '/ontime/settings' endpoint
|
||||||
router.get('/settings', getSettings);
|
router.get('/settings', getSettings);
|
||||||
@@ -85,12 +83,6 @@ router.get('/aliases', getAliases);
|
|||||||
// create route between controller and '/ontime/aliases' endpoint
|
// create route between controller and '/ontime/aliases' endpoint
|
||||||
router.post('/aliases', validateAliases, postAliases);
|
router.post('/aliases', validateAliases, postAliases);
|
||||||
|
|
||||||
// create route between controller and '/ontime/aliases' endpoint
|
|
||||||
router.get('/userfields', getUserFields);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/aliases' endpoint
|
|
||||||
router.post('/userfields', validateUserFields, postUserFields);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/info' endpoint
|
// create route between controller and '/ontime/info' endpoint
|
||||||
router.get('/info', getInfo);
|
router.get('/info', getInfo);
|
||||||
|
|
||||||
|
|||||||
@@ -408,16 +408,6 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
@@ -446,16 +436,6 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
@@ -484,16 +464,6 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
@@ -522,16 +492,6 @@ describe('calculateRuntimeDelays', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
@@ -569,16 +529,6 @@ describe('getDelayAt()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
@@ -608,16 +558,6 @@ describe('getDelayAt()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
@@ -647,16 +587,6 @@ describe('getDelayAt()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
@@ -686,16 +616,6 @@ describe('getDelayAt()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
@@ -751,16 +671,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
@@ -790,16 +700,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
@@ -829,16 +729,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
@@ -868,16 +758,6 @@ describe('calculateRuntimeDelaysFrom()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 120000,
|
timeWarning: 120000,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export async function setRundown(initialRundown: OntimeRundown) {
|
|||||||
generate();
|
generate();
|
||||||
await DataProvider.setRundown(persistedRundown);
|
await DataProvider.setRundown(persistedRundown);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility initialises cache
|
* Utility initialises cache
|
||||||
* @param rundown
|
* @param rundown
|
||||||
@@ -89,10 +90,6 @@ export function generate(
|
|||||||
// 1. handle links
|
// 1. handle links
|
||||||
handleLink(i, initialRundown, updatedEvent, links);
|
handleLink(i, initialRundown, updatedEvent, links);
|
||||||
|
|
||||||
// TODO: wait until the next thing?
|
|
||||||
// update the persisted event
|
|
||||||
initialRundown[i] = updatedEvent;
|
|
||||||
|
|
||||||
// 2. handle custom fields
|
// 2. handle custom fields
|
||||||
handleCustomField(customFields, customFieldChangelog, updatedEvent, assignedCustomFields);
|
handleCustomField(customFields, customFieldChangelog, updatedEvent, assignedCustomFields);
|
||||||
|
|
||||||
@@ -196,14 +193,12 @@ export function mutateCache<T extends object>(mutation: MutatingFn<T>) {
|
|||||||
console.timeEnd('rundownCache__init');
|
console.timeEnd('rundownCache__init');
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: should we trottle this?
|
// TODO: should we throttle this?
|
||||||
// defer writing to the database
|
// defer writing to the database
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
console.log('writing to database', persistedRundown.length);
|
|
||||||
DataProvider.setRundown(persistedRundown);
|
DataProvider.setRundown(persistedRundown);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: could we return a patch object?
|
|
||||||
return { newEvent };
|
return { newEvent };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* @link https://developers.google.com/identity/protocols/oauth2/limited-input-device
|
* @link https://developers.google.com/identity/protocols/oauth2/limited-input-device
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AuthenticationStatus, LogOrigin, MaybeString, OntimeRundown, UserFields } from 'ontime-types';
|
import { AuthenticationStatus, CustomFields, LogOrigin, MaybeString, OntimeRundown } from 'ontime-types';
|
||||||
|
|
||||||
import { sheets, sheets_v4 } from '@googleapis/sheets';
|
import { sheets, sheets_v4 } from '@googleapis/sheets';
|
||||||
import { Credentials, OAuth2Client } from 'google-auth-library';
|
import { Credentials, OAuth2Client } from 'google-auth-library';
|
||||||
@@ -13,10 +13,10 @@ import got from 'got';
|
|||||||
import { resolveSheetsDirectory } from '../../setup.js';
|
import { resolveSheetsDirectory } from '../../setup.js';
|
||||||
import { ensureDirectory } from '../../utils/fileManagement.js';
|
import { ensureDirectory } from '../../utils/fileManagement.js';
|
||||||
import { type ClientSecret, cellRequestFromEvent, getA1Notation, validateClientSecret } from './sheetUtils.js';
|
import { type ClientSecret, cellRequestFromEvent, getA1Notation, validateClientSecret } from './sheetUtils.js';
|
||||||
import { ExcelImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
import { parseExcel } from '../../utils/parser.js';
|
import { parseExcel } from '../../utils/parser.js';
|
||||||
import { logger } from '../../classes/Logger.js';
|
import { logger } from '../../classes/Logger.js';
|
||||||
import { parseRundown, parseUserFields } from '../../utils/parserFunctions.js';
|
import { parseCustomFields, parseRundown } from '../../utils/parserFunctions.js';
|
||||||
import { getRundown } from '../rundown-service/rundownUtils.js';
|
import { getRundown } from '../rundown-service/rundownUtils.js';
|
||||||
|
|
||||||
const sheetScope = 'https://www.googleapis.com/auth/spreadsheets';
|
const sheetScope = 'https://www.googleapis.com/auth/spreadsheets';
|
||||||
@@ -258,7 +258,9 @@ async function verifyWorksheet(sheetId: string, worksheet: string): Promise<{ wo
|
|||||||
throw new Error(`Request failed: ${spreadsheets.status} ${spreadsheets.statusText}`);
|
throw new Error(`Request failed: ${spreadsheets.status} ${spreadsheets.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedWorksheet = spreadsheets.data.sheets.find((n) => n.properties.title == worksheet);
|
const selectedWorksheet = spreadsheets.data.sheets.find(
|
||||||
|
(n) => n.properties.title.toLowerCase() === worksheet.toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
if (!selectedWorksheet) {
|
if (!selectedWorksheet) {
|
||||||
throw new Error('Could not find worksheet');
|
throw new Error('Could not find worksheet');
|
||||||
@@ -271,7 +273,7 @@ async function verifyWorksheet(sheetId: string, worksheet: string): Promise<{ wo
|
|||||||
return { worksheetId: selectedWorksheet.properties.sheetId, range: `${worksheet}!A1:${endCell}` };
|
return { worksheetId: selectedWorksheet.properties.sheetId, range: `${worksheet}!A1:${endCell}` };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upload(sheetId: string, options: ExcelImportMap) {
|
export async function upload(sheetId: string, options: ImportMap) {
|
||||||
const { worksheetId, range } = await verifyWorksheet(sheetId, options.worksheet);
|
const { worksheetId, range } = await verifyWorksheet(sheetId, options.worksheet);
|
||||||
|
|
||||||
const readResponse = await sheets({ version: 'v4', auth: currentAuthClient }).spreadsheets.values.get({
|
const readResponse = await sheets({ version: 'v4', auth: currentAuthClient }).spreadsheets.values.get({
|
||||||
@@ -344,10 +346,10 @@ export async function upload(sheetId: string, options: ExcelImportMap) {
|
|||||||
|
|
||||||
export async function download(
|
export async function download(
|
||||||
sheetId: string,
|
sheetId: string,
|
||||||
options: ExcelImportMap,
|
options: ImportMap,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
rundown: OntimeRundown;
|
rundown: OntimeRundown;
|
||||||
userFields: UserFields;
|
customFields: CustomFields;
|
||||||
}> {
|
}> {
|
||||||
const { range } = await verifyWorksheet(sheetId, options.worksheet);
|
const { range } = await verifyWorksheet(sheetId, options.worksheet);
|
||||||
|
|
||||||
@@ -367,6 +369,6 @@ export async function download(
|
|||||||
if (rundown.length < 1) {
|
if (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');
|
||||||
}
|
}
|
||||||
const userFields = parseUserFields(dataFromSheet);
|
const customFields = parseCustomFields(dataFromSheet);
|
||||||
return { rundown, userFields };
|
return { rundown, customFields };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,16 +37,6 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: 'red',
|
colour: 'red',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
id: '1358',
|
id: '1358',
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
@@ -68,16 +58,6 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
isPublic: { row: 1, col: 25 },
|
isPublic: { row: 1, col: 25 },
|
||||||
skip: { row: 1, col: 26 },
|
skip: { row: 1, col: 26 },
|
||||||
colour: { row: 1, col: 27 },
|
colour: { row: 1, col: 27 },
|
||||||
user0: { row: 1, col: 28 },
|
|
||||||
user1: { row: 1, col: 29 },
|
|
||||||
user2: { row: 1, col: 30 },
|
|
||||||
user3: { row: 1, col: 31 },
|
|
||||||
user4: { row: 1, col: 32 },
|
|
||||||
user5: { row: 1, col: 33 },
|
|
||||||
user6: { row: 1, col: 34 },
|
|
||||||
user7: { row: 1, col: 35 },
|
|
||||||
user8: { row: 1, col: 36 },
|
|
||||||
user9: { row: 1, col: 37 },
|
|
||||||
revision: { row: 1, col: 38 },
|
revision: { row: 1, col: 38 },
|
||||||
id: { row: 1, col: 39 },
|
id: { row: 1, col: 39 },
|
||||||
timeWarning: { row: 1, col: 40 },
|
timeWarning: { row: 1, col: 40 },
|
||||||
@@ -87,7 +67,7 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
expect(result.updateCells.rows[0].values[5].userEnteredValue.stringValue).toStrictEqual(event.note);
|
expect(result.updateCells.rows[0].values[5].userEnteredValue.stringValue).toStrictEqual(event.note);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('numer to timer', () => {
|
test('number to timer', () => {
|
||||||
const event: OntimeEvent = {
|
const event: OntimeEvent = {
|
||||||
type: SupportedEvent.Event,
|
type: SupportedEvent.Event,
|
||||||
cue: '1',
|
cue: '1',
|
||||||
@@ -105,16 +85,6 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: 'red',
|
colour: 'red',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
id: '1358',
|
id: '1358',
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
@@ -136,16 +106,6 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
isPublic: { row: 1, col: 25 },
|
isPublic: { row: 1, col: 25 },
|
||||||
skip: { row: 1, col: 26 },
|
skip: { row: 1, col: 26 },
|
||||||
colour: { row: 1, col: 27 },
|
colour: { row: 1, col: 27 },
|
||||||
user0: { row: 1, col: 28 },
|
|
||||||
user1: { row: 1, col: 29 },
|
|
||||||
user2: { row: 1, col: 30 },
|
|
||||||
user3: { row: 1, col: 31 },
|
|
||||||
user4: { row: 1, col: 32 },
|
|
||||||
user5: { row: 1, col: 33 },
|
|
||||||
user6: { row: 1, col: 34 },
|
|
||||||
user7: { row: 1, col: 35 },
|
|
||||||
user8: { row: 1, col: 36 },
|
|
||||||
user9: { row: 1, col: 37 },
|
|
||||||
revision: { row: 1, col: 38 },
|
revision: { row: 1, col: 38 },
|
||||||
id: { row: 1, col: 39 },
|
id: { row: 1, col: 39 },
|
||||||
timeWarning: { row: 1, col: 40 },
|
timeWarning: { row: 1, col: 40 },
|
||||||
@@ -174,16 +134,6 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: 'red',
|
colour: 'red',
|
||||||
user0: 'u',
|
|
||||||
user1: 'u',
|
|
||||||
user2: 'u',
|
|
||||||
user3: 'u',
|
|
||||||
user4: 'u',
|
|
||||||
user5: 'u',
|
|
||||||
user6: 'u',
|
|
||||||
user7: 'u',
|
|
||||||
user8: 'u',
|
|
||||||
user9: 'u',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
id: '1358',
|
id: '1358',
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
@@ -205,16 +155,6 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
isPublic: { row: 1, col: 25 },
|
isPublic: { row: 1, col: 25 },
|
||||||
skip: { row: 1, col: 26 },
|
skip: { row: 1, col: 26 },
|
||||||
colour: { row: 1, col: 27 },
|
colour: { row: 1, col: 27 },
|
||||||
user0: { row: 1, col: 28 },
|
|
||||||
user1: { row: 1, col: 29 },
|
|
||||||
user2: { row: 1, col: 30 },
|
|
||||||
user3: { row: 1, col: 31 },
|
|
||||||
user4: { row: 1, col: 32 },
|
|
||||||
user5: { row: 1, col: 33 },
|
|
||||||
user6: { row: 1, col: 34 },
|
|
||||||
user7: { row: 1, col: 35 },
|
|
||||||
user8: { row: 1, col: 36 },
|
|
||||||
user9: { row: 1, col: 37 },
|
|
||||||
revision: { row: 1, col: 38 },
|
revision: { row: 1, col: 38 },
|
||||||
id: { row: 1, col: 39 },
|
id: { row: 1, col: 39 },
|
||||||
timeWarning: { row: 1, col: 40 },
|
timeWarning: { row: 1, col: 40 },
|
||||||
@@ -243,16 +183,6 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: 'red',
|
colour: 'red',
|
||||||
user0: 'u',
|
|
||||||
user1: 'u',
|
|
||||||
user2: 'u',
|
|
||||||
user3: 'u',
|
|
||||||
user4: 'u',
|
|
||||||
user5: 'u',
|
|
||||||
user6: 'u',
|
|
||||||
user7: 'u',
|
|
||||||
user8: 'u',
|
|
||||||
user9: 'u',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
id: '1358',
|
id: '1358',
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
@@ -263,7 +193,6 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
cue: { row: 1, col: 0 },
|
cue: { row: 1, col: 0 },
|
||||||
title: { row: 1, col: 6 },
|
title: { row: 1, col: 6 },
|
||||||
subtitle: { row: 1, col: 10 },
|
subtitle: { row: 1, col: 10 },
|
||||||
user0: { row: 1, col: 16 },
|
|
||||||
};
|
};
|
||||||
const result = cellRequestFromEvent(event, 1, 1234, metadata);
|
const result = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||||
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue);
|
expect(result.updateCells.rows[0].values[0].userEnteredValue.stringValue).toStrictEqual(event.cue);
|
||||||
@@ -289,16 +218,6 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: 'red',
|
colour: 'red',
|
||||||
user0: 'u',
|
|
||||||
user1: 'u',
|
|
||||||
user2: 'u',
|
|
||||||
user3: 'u',
|
|
||||||
user4: 'u',
|
|
||||||
user5: 'u',
|
|
||||||
user6: 'u',
|
|
||||||
user7: 'u',
|
|
||||||
user8: 'u',
|
|
||||||
user9: 'u',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
id: '1358',
|
id: '1358',
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
@@ -335,16 +254,6 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: 'red',
|
colour: 'red',
|
||||||
user0: 'u',
|
|
||||||
user1: 'u',
|
|
||||||
user2: 'u',
|
|
||||||
user3: 'u',
|
|
||||||
user4: 'u',
|
|
||||||
user5: 'u',
|
|
||||||
user6: 'u',
|
|
||||||
user7: 'u',
|
|
||||||
user8: 'u',
|
|
||||||
user9: 'u',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
id: '1358',
|
id: '1358',
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
@@ -355,7 +264,6 @@ describe('cellRequestFromEvent()', () => {
|
|||||||
cue: { row: 10, col: 5 },
|
cue: { row: 10, col: 5 },
|
||||||
title: { row: 10, col: 6 },
|
title: { row: 10, col: 6 },
|
||||||
subtitle: { row: 1, col: 10 },
|
subtitle: { row: 1, col: 10 },
|
||||||
user0: { row: 10, col: 16 },
|
|
||||||
};
|
};
|
||||||
const result1 = cellRequestFromEvent(event, 1, 1234, metadata);
|
const result1 = cellRequestFromEvent(event, 1, 1234, metadata);
|
||||||
expect(result1.updateCells.start.sheetId).toStrictEqual(1234);
|
expect(result1.updateCells.start.sheetId).toStrictEqual(1234);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable no-console -- we are mocking the console */
|
/* eslint-disable no-console -- we are mocking the console */
|
||||||
import { vi } from 'vitest';
|
import { assertType, vi } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DatabaseModel,
|
DatabaseModel,
|
||||||
@@ -11,12 +11,14 @@ import {
|
|||||||
TimeStrategy,
|
TimeStrategy,
|
||||||
TimerType,
|
TimerType,
|
||||||
ViewSettings,
|
ViewSettings,
|
||||||
|
OntimeRundown,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
import { dbModel } from '../../models/dataModel.js';
|
import { dbModel } from '../../models/dataModel.js';
|
||||||
import { parseExcel, parseJson, createEvent } from '../parser.js';
|
|
||||||
|
import { parseExcel, parseJson, createEvent, getCustomFieldData } from '../parser.js';
|
||||||
import { makeString } from '../parserUtils.js';
|
import { makeString } from '../parserUtils.js';
|
||||||
import { parseAliases, parseUserFields, parseViewSettings } from '../parserFunctions.js';
|
import { parseAliases, parseViewSettings } from '../parserFunctions.js';
|
||||||
|
|
||||||
describe('test json parser with valid def', () => {
|
describe('test json parser with valid def', () => {
|
||||||
const testData: Partial<DatabaseModel> = {
|
const testData: Partial<DatabaseModel> = {
|
||||||
@@ -39,16 +41,6 @@ describe('test json parser with valid def', () => {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
timeDanger: 0,
|
timeDanger: 0,
|
||||||
@@ -72,16 +64,6 @@ describe('test json parser with valid def', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: true,
|
skip: true,
|
||||||
colour: 'red',
|
colour: 'red',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
timeDanger: 0,
|
timeDanger: 0,
|
||||||
@@ -105,16 +87,6 @@ describe('test json parser with valid def', () => {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
timeDanger: 0,
|
timeDanger: 0,
|
||||||
@@ -159,16 +131,6 @@ describe('test json parser with valid def', () => {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
skip: false,
|
skip: false,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
timeDanger: 0,
|
timeDanger: 0,
|
||||||
@@ -192,16 +154,6 @@ describe('test json parser with valid def', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: true,
|
skip: true,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
timeDanger: 0,
|
timeDanger: 0,
|
||||||
@@ -223,21 +175,11 @@ describe('test json parser with valid def', () => {
|
|||||||
duration: 37200000 - 32400000,
|
duration: 37200000 - 32400000,
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
colour: '',
|
colour: '',
|
||||||
user0: '',
|
|
||||||
user1: '',
|
|
||||||
user2: '',
|
|
||||||
user3: '',
|
|
||||||
user4: '',
|
|
||||||
user5: '',
|
|
||||||
user6: '',
|
|
||||||
user7: '',
|
|
||||||
user8: '',
|
|
||||||
user9: '',
|
|
||||||
revision: 0,
|
revision: 0,
|
||||||
timeWarning: 0,
|
timeWarning: 0,
|
||||||
timeDanger: 0,
|
timeDanger: 0,
|
||||||
} as OntimeEvent,
|
} as OntimeEvent,
|
||||||
],
|
] as OntimeRundown,
|
||||||
project: {
|
project: {
|
||||||
title: 'This is a test definition',
|
title: 'This is a test definition',
|
||||||
backstageUrl: 'www.carlosvalente.com',
|
backstageUrl: 'www.carlosvalente.com',
|
||||||
@@ -540,16 +482,7 @@ describe('test event validator', () => {
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
cue: 'test',
|
cue: 'test',
|
||||||
colour: expect.any(String),
|
colour: expect.any(String),
|
||||||
user0: expect.any(String),
|
custom: expect.any(Object),
|
||||||
user1: expect.any(String),
|
|
||||||
user2: expect.any(String),
|
|
||||||
user3: expect.any(String),
|
|
||||||
user4: expect.any(String),
|
|
||||||
user5: expect.any(String),
|
|
||||||
user6: expect.any(String),
|
|
||||||
user7: expect.any(String),
|
|
||||||
user8: expect.any(String),
|
|
||||||
user9: expect.any(String),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -600,7 +533,197 @@ describe('test event validator', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('test makeString function', () => {
|
describe('test aliases import', () => {
|
||||||
|
it('imports a well defined alias', () => {
|
||||||
|
const testData = {
|
||||||
|
rundown: [],
|
||||||
|
settings: {
|
||||||
|
app: 'ontime',
|
||||||
|
version: '2.0.0',
|
||||||
|
},
|
||||||
|
aliases: [
|
||||||
|
{
|
||||||
|
enabled: false,
|
||||||
|
alias: 'testalias',
|
||||||
|
pathAndParams: 'testpathAndParams',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsed = parseAliases(testData);
|
||||||
|
expect(parsed.length).toBe(1);
|
||||||
|
|
||||||
|
// generates missing id
|
||||||
|
expect(parsed[0].alias).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('test views import', () => {
|
||||||
|
it('imports data from file', () => {
|
||||||
|
const testData = {
|
||||||
|
rundown: [],
|
||||||
|
settings: {
|
||||||
|
app: 'ontime',
|
||||||
|
version: '2.0.0',
|
||||||
|
},
|
||||||
|
viewSettings: {
|
||||||
|
normalColor: '#ffffffcc',
|
||||||
|
warningColor: '#FFAB33',
|
||||||
|
dangerColor: '#ED3333',
|
||||||
|
endMessage: '',
|
||||||
|
overrideStyles: false,
|
||||||
|
notAthing: true,
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
overrideStyles: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expectedParsedViewSettings = {
|
||||||
|
normalColor: '#ffffffcc',
|
||||||
|
warningColor: '#FFAB33',
|
||||||
|
dangerColor: '#ED3333',
|
||||||
|
endMessage: '',
|
||||||
|
overrideStyles: false,
|
||||||
|
};
|
||||||
|
const parsed = parseViewSettings(testData);
|
||||||
|
expect(parsed).toStrictEqual(expectedParsedViewSettings);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('imports defaults to model', () => {
|
||||||
|
const testData = {
|
||||||
|
rundown: [],
|
||||||
|
settings: {
|
||||||
|
app: 'ontime',
|
||||||
|
version: '2.0.0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const parsed = parseViewSettings(testData);
|
||||||
|
expect(parsed).toStrictEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('test import of v2 datamodel', () => {
|
||||||
|
it('ignores deprecated fields and generates new ones', async () => {
|
||||||
|
const v2ProjectFile = {
|
||||||
|
rundown: [
|
||||||
|
{ type: SupportedEvent.Block, title: 'block-title', id: 'block-id' },
|
||||||
|
{ type: SupportedEvent.Delay, duration: 0 },
|
||||||
|
{ type: SupportedEvent.Event, title: 'block-title', id: 'block-id' },
|
||||||
|
],
|
||||||
|
project: {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
publicUrl: '',
|
||||||
|
publicInfo: '',
|
||||||
|
backstageUrl: '',
|
||||||
|
backstageInfo: '',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
app: 'ontime',
|
||||||
|
version: '2.0.0',
|
||||||
|
serverPort: 4001,
|
||||||
|
editorKey: null,
|
||||||
|
operatorKey: null,
|
||||||
|
timeFormat: '24',
|
||||||
|
language: 'en',
|
||||||
|
},
|
||||||
|
viewSettings: {
|
||||||
|
overrideStyles: false,
|
||||||
|
normalColor: '#ffffffcc',
|
||||||
|
warningColor: '#FFAB33',
|
||||||
|
dangerColor: '#ED3333',
|
||||||
|
endMessage: '',
|
||||||
|
},
|
||||||
|
aliases: [],
|
||||||
|
userFields: {
|
||||||
|
user0: 'user0',
|
||||||
|
user1: 'user1',
|
||||||
|
user2: 'user2',
|
||||||
|
user3: 'user3',
|
||||||
|
user4: 'user4',
|
||||||
|
user5: 'user5',
|
||||||
|
user6: 'user6',
|
||||||
|
user7: 'user7',
|
||||||
|
user8: 'user8',
|
||||||
|
user9: 'user9',
|
||||||
|
},
|
||||||
|
osc: {
|
||||||
|
portIn: 8888,
|
||||||
|
portOut: 9999,
|
||||||
|
targetIP: '127.0.0.1',
|
||||||
|
enabledIn: false,
|
||||||
|
enabledOut: false,
|
||||||
|
subscriptions: {
|
||||||
|
onLoad: [],
|
||||||
|
onStart: [],
|
||||||
|
onPause: [],
|
||||||
|
onStop: [],
|
||||||
|
onUpdate: [],
|
||||||
|
onFinish: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
http: {
|
||||||
|
enabledOut: false,
|
||||||
|
subscriptions: {
|
||||||
|
onLoad: [],
|
||||||
|
onStart: [],
|
||||||
|
onPause: [],
|
||||||
|
onStop: [],
|
||||||
|
onUpdate: [],
|
||||||
|
onFinish: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// @ts-expect-error -- we know this is wrong, testing imports outside domain
|
||||||
|
const parsed = await parseJson(v2ProjectFile);
|
||||||
|
expect(parsed.rundown.length).toBe(3);
|
||||||
|
expect(parsed.rundown[0]).toMatchObject({ type: SupportedEvent.Block });
|
||||||
|
expect(parsed.rundown[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: expect.any(String),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(parsed.rundown[1]).toMatchObject({ type: SupportedEvent.Delay });
|
||||||
|
expect(parsed.rundown[1]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
duration: expect.any(Number),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(parsed.rundown[2]).toMatchObject({ type: SupportedEvent.Event });
|
||||||
|
expect(parsed.rundown[2]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(String),
|
||||||
|
cue: expect.any(String),
|
||||||
|
title: expect.any(String),
|
||||||
|
subtitle: expect.any(String),
|
||||||
|
presenter: expect.any(String),
|
||||||
|
note: expect.any(String),
|
||||||
|
endAction: expect.any(String),
|
||||||
|
timerType: expect.any(String),
|
||||||
|
linkStart: null,
|
||||||
|
timeStrategy: expect.any(String),
|
||||||
|
timeStart: expect.any(Number),
|
||||||
|
timeEnd: expect.any(Number),
|
||||||
|
duration: expect.any(Number),
|
||||||
|
isPublic: expect.any(Boolean),
|
||||||
|
skip: expect.any(Boolean),
|
||||||
|
colour: expect.any(String),
|
||||||
|
revision: expect.any(Number),
|
||||||
|
timeWarning: expect.any(Number),
|
||||||
|
timeDanger: expect.any(Number),
|
||||||
|
custom: expect.any(Object),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// @ts-expect-error -- checking if the field is removed
|
||||||
|
expect(parsed?.userFields).toBeUndefined();
|
||||||
|
expect(parsed.osc).toMatchObject({ subscriptions: [] });
|
||||||
|
expect(parsed.http).toMatchObject({ enabledOut: false, subscriptions: [] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('makeString()', () => {
|
||||||
it('converts variables to string', () => {
|
it('converts variables to string', () => {
|
||||||
let val = 2;
|
let val = 2;
|
||||||
let expected = '2';
|
let expected = '2';
|
||||||
@@ -625,7 +748,61 @@ describe('test makeString function', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('test parseExcel function', () => {
|
describe('getCustomFieldData()', () => {
|
||||||
|
it('generates a list of keys from the given import map', () => {
|
||||||
|
const importMap = {
|
||||||
|
worksheet: 'event schedule',
|
||||||
|
timeStart: 'time start',
|
||||||
|
timeEnd: 'time end',
|
||||||
|
duration: 'duration',
|
||||||
|
cue: 'cue',
|
||||||
|
title: 'title',
|
||||||
|
presenter: 'presenter',
|
||||||
|
subtitle: 'subtitle',
|
||||||
|
isPublic: 'public',
|
||||||
|
skip: 'skip',
|
||||||
|
note: 'notes',
|
||||||
|
colour: 'colour',
|
||||||
|
endAction: 'end action',
|
||||||
|
timerType: 'timer type',
|
||||||
|
timeWarning: 'warning time',
|
||||||
|
timeDanger: 'danger time',
|
||||||
|
custom: {
|
||||||
|
lighting: 'lx',
|
||||||
|
sound: 'sound',
|
||||||
|
video: 'av',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getCustomFieldData(importMap);
|
||||||
|
expect(result.customFields).toStrictEqual({
|
||||||
|
lighting: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'lighting',
|
||||||
|
},
|
||||||
|
sound: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'sound',
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'video',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// it is an inverted record of <importKey, ontimeKey>
|
||||||
|
expect(result.customFieldImportKeys).toStrictEqual({
|
||||||
|
lx: 'lighting',
|
||||||
|
sound: 'sound',
|
||||||
|
av: 'video',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseExcel()', () => {
|
||||||
it('parses the example file', async () => {
|
it('parses the example file', async () => {
|
||||||
const testdata = [
|
const testdata = [
|
||||||
['Ontime ┬À Schedule Template'],
|
['Ontime ┬À Schedule Template'],
|
||||||
@@ -705,17 +882,20 @@ describe('test parseExcel function', () => {
|
|||||||
[],
|
[],
|
||||||
];
|
];
|
||||||
|
|
||||||
const partialOptions = {
|
// partial import map with only custom fields
|
||||||
user0: 'test0',
|
const importMap = {
|
||||||
user1: 'test1',
|
custom: {
|
||||||
user2: 'test2',
|
user0: 'test0',
|
||||||
user3: 'test3',
|
user1: 'test1',
|
||||||
user4: 'test4',
|
user2: 'test2',
|
||||||
user5: 'test5',
|
user3: 'test3',
|
||||||
user6: 'test6',
|
user4: 'test4',
|
||||||
user7: 'test7',
|
user5: 'test5',
|
||||||
user8: 'test8',
|
user6: 'test6',
|
||||||
user9: 'test9',
|
user7: 'test7',
|
||||||
|
user8: 'test8',
|
||||||
|
user9: 'test9',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: update tests once import is resolved
|
// TODO: update tests once import is resolved
|
||||||
@@ -731,16 +911,18 @@ describe('test parseExcel function', () => {
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
skip: false,
|
skip: false,
|
||||||
note: 'Ballyhoo',
|
note: 'Ballyhoo',
|
||||||
user0: 'a0',
|
custom: {
|
||||||
user1: 'a1',
|
user0: { value: 'a0' },
|
||||||
user2: 'a2',
|
user1: { value: 'a1' },
|
||||||
user3: 'a3',
|
user2: { value: 'a2' },
|
||||||
user4: 'a4',
|
user3: { value: 'a3' },
|
||||||
user5: 'a5',
|
user4: { value: 'a4' },
|
||||||
user6: 'a6',
|
user5: { value: 'a5' },
|
||||||
user7: 'a7',
|
user6: { value: 'a6' },
|
||||||
user8: 'a8',
|
user7: { value: 'a7' },
|
||||||
user9: 'a9',
|
user8: { value: 'a8' },
|
||||||
|
user9: { value: 'a9' },
|
||||||
|
},
|
||||||
colour: 'red',
|
colour: 'red',
|
||||||
type: 'event',
|
type: 'event',
|
||||||
cue: '101',
|
cue: '101',
|
||||||
@@ -756,174 +938,214 @@ describe('test parseExcel function', () => {
|
|||||||
isPublic: false,
|
isPublic: false,
|
||||||
skip: true,
|
skip: true,
|
||||||
note: 'Rainbow chase',
|
note: 'Rainbow chase',
|
||||||
user0: 'b0',
|
custom: {
|
||||||
user5: 'b5',
|
user0: { value: 'b0' },
|
||||||
|
user5: { value: 'b5' },
|
||||||
|
},
|
||||||
colour: '#F00',
|
colour: '#F00',
|
||||||
type: 'event',
|
type: 'event',
|
||||||
cue: '102',
|
cue: '102',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const parsedData = parseExcel(testdata, partialOptions);
|
const parsedData = parseExcel(testdata, importMap);
|
||||||
expect(parsedData.rundown).toBeDefined();
|
expect(parsedData.customFields).toStrictEqual({
|
||||||
|
user0: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'user0',
|
||||||
|
},
|
||||||
|
user1: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'user1',
|
||||||
|
},
|
||||||
|
user2: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'user2',
|
||||||
|
},
|
||||||
|
user3: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'user3',
|
||||||
|
},
|
||||||
|
user4: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'user4',
|
||||||
|
},
|
||||||
|
user5: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'user5',
|
||||||
|
},
|
||||||
|
user6: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'user6',
|
||||||
|
},
|
||||||
|
user7: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'user7',
|
||||||
|
},
|
||||||
|
user8: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'user8',
|
||||||
|
},
|
||||||
|
user9: {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: 'user9',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(parsedData.rundown.length).toBe(2);
|
||||||
expect(parsedData.rundown[0]).toMatchObject(expectedParsedRundown[0]);
|
expect(parsedData.rundown[0]).toMatchObject(expectedParsedRundown[0]);
|
||||||
expect(parsedData.rundown[1]).toMatchObject(expectedParsedRundown[1]);
|
expect(parsedData.rundown[1]).toMatchObject(expectedParsedRundown[1]);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
describe('test aliases import', () => {
|
it('parses a file without custom fields', async () => {
|
||||||
it('imports a well defined alias', () => {
|
const testdata = [
|
||||||
const testData = {
|
['Ontime ┬À Schedule Template'],
|
||||||
rundown: [],
|
[],
|
||||||
settings: {
|
[
|
||||||
app: 'ontime',
|
'Time Start',
|
||||||
version: '2.0.0',
|
'Time End',
|
||||||
},
|
'Title',
|
||||||
aliases: [
|
'Presenter',
|
||||||
{
|
'Subtitle',
|
||||||
enabled: false,
|
'End Action',
|
||||||
alias: 'testalias',
|
'Timer type',
|
||||||
pathAndParams: 'testpathAndParams',
|
'Public',
|
||||||
},
|
'Skip',
|
||||||
|
'Notes',
|
||||||
|
'test0',
|
||||||
|
'test1',
|
||||||
|
'test2',
|
||||||
|
'test3',
|
||||||
|
'test4',
|
||||||
|
'test5',
|
||||||
|
'test6',
|
||||||
|
'test7',
|
||||||
|
'test8',
|
||||||
|
'test9',
|
||||||
|
'Colour',
|
||||||
|
'cue',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'1899-12-30T07:00:00.000Z',
|
||||||
|
'1899-12-30T08:00:10.000Z',
|
||||||
|
'Guest Welcome',
|
||||||
|
'Carlos',
|
||||||
|
'Getting things started',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'x',
|
||||||
|
'',
|
||||||
|
'Ballyhoo',
|
||||||
|
'a0',
|
||||||
|
'a1',
|
||||||
|
'a2',
|
||||||
|
'a3',
|
||||||
|
'a4',
|
||||||
|
'a5',
|
||||||
|
'a6',
|
||||||
|
'a7',
|
||||||
|
'a8',
|
||||||
|
'a9',
|
||||||
|
'red',
|
||||||
|
101,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'1899-12-30T08:00:00.000Z',
|
||||||
|
'1899-12-30T08:30:00.000Z',
|
||||||
|
'A song from the hearth',
|
||||||
|
'Still Carlos',
|
||||||
|
'Derailing early',
|
||||||
|
'load-next',
|
||||||
|
'clock',
|
||||||
|
'',
|
||||||
|
'x',
|
||||||
|
'Rainbow chase',
|
||||||
|
'b0',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'b5',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'#F00',
|
||||||
|
102,
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
|
||||||
|
// partial import map with only custom fields
|
||||||
|
const importMap = {
|
||||||
|
custom: {
|
||||||
|
niu1: 'niu1',
|
||||||
|
niu2: 'niu2',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsed = parseAliases(testData);
|
// TODO: update tests once import is resolved
|
||||||
expect(parsed.length).toBe(1);
|
const expectedParsedRundown = [
|
||||||
|
{
|
||||||
|
//timeStart: 28800000,
|
||||||
|
//timeEnd: 32410000,
|
||||||
|
title: 'Guest Welcome',
|
||||||
|
presenter: 'Carlos',
|
||||||
|
subtitle: 'Getting things started',
|
||||||
|
timerType: 'count-down',
|
||||||
|
endAction: 'none',
|
||||||
|
isPublic: true,
|
||||||
|
skip: false,
|
||||||
|
note: 'Ballyhoo',
|
||||||
|
custom: {},
|
||||||
|
colour: 'red',
|
||||||
|
type: 'event',
|
||||||
|
cue: '101',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
//timeStart: 32400000,
|
||||||
|
//timeEnd: 34200000,
|
||||||
|
title: 'A song from the hearth',
|
||||||
|
presenter: 'Still Carlos',
|
||||||
|
subtitle: 'Derailing early',
|
||||||
|
timerType: 'clock',
|
||||||
|
endAction: 'load-next',
|
||||||
|
isPublic: false,
|
||||||
|
skip: true,
|
||||||
|
note: 'Rainbow chase',
|
||||||
|
custom: {},
|
||||||
|
colour: '#F00',
|
||||||
|
type: 'event',
|
||||||
|
cue: '102',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// generates missing id
|
const parsedData = parseExcel(testdata, importMap);
|
||||||
expect(parsed[0].alias).toBeDefined();
|
expect(parsedData.customFields).toStrictEqual({
|
||||||
});
|
niu1: {
|
||||||
});
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
describe('test userFields import', () => {
|
label: 'niu1',
|
||||||
const model = dbModel.userFields;
|
},
|
||||||
it('imports a fully defined user fields', () => {
|
niu2: {
|
||||||
const testUserFields = {
|
type: 'string',
|
||||||
user0: 'test0',
|
colour: '',
|
||||||
user1: 'test1',
|
label: 'niu2',
|
||||||
user2: 'test2',
|
},
|
||||||
user3: 'test3',
|
});
|
||||||
user4: 'test4',
|
expect(parsedData.rundown.length).toBe(2);
|
||||||
user5: 'test5',
|
expect(parsedData.rundown[0]).toMatchObject(expectedParsedRundown[0]);
|
||||||
user6: 'test6',
|
expect(parsedData.rundown[1]).toMatchObject(expectedParsedRundown[1]);
|
||||||
user7: 'test7',
|
|
||||||
user8: 'test8',
|
|
||||||
user9: 'test9',
|
|
||||||
};
|
|
||||||
|
|
||||||
const testData = {
|
|
||||||
rundown: [],
|
|
||||||
settings: {
|
|
||||||
app: 'ontime',
|
|
||||||
version: '2.0.0',
|
|
||||||
},
|
|
||||||
userFields: testUserFields,
|
|
||||||
};
|
|
||||||
|
|
||||||
const parsed = parseUserFields(testData);
|
|
||||||
expect(parsed).toStrictEqual(testUserFields);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('imports a partially defined user fields', () => {
|
|
||||||
const testUserFields = {
|
|
||||||
user0: 'test0',
|
|
||||||
user1: 'test1',
|
|
||||||
user7: 'test7',
|
|
||||||
user8: 'test8',
|
|
||||||
user9: 'test9',
|
|
||||||
};
|
|
||||||
|
|
||||||
const expected = {
|
|
||||||
...model,
|
|
||||||
...testUserFields,
|
|
||||||
};
|
|
||||||
|
|
||||||
const testData = {
|
|
||||||
rundown: [],
|
|
||||||
settings: {
|
|
||||||
app: 'ontime',
|
|
||||||
version: '2.0.0',
|
|
||||||
},
|
|
||||||
userFields: testUserFields,
|
|
||||||
};
|
|
||||||
|
|
||||||
const parsed = parseUserFields(testData);
|
|
||||||
expect(parsed).toStrictEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles missing user fields', () => {
|
|
||||||
const testData = {
|
|
||||||
rundown: [],
|
|
||||||
settings: {
|
|
||||||
app: 'ontime',
|
|
||||||
version: '2.0.0',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const parsed = parseUserFields(testData);
|
|
||||||
expect(parsed).toStrictEqual(model);
|
|
||||||
expect(parsed).toStrictEqual(model);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores badly defined fields', () => {
|
|
||||||
const testData = {
|
|
||||||
rundown: [],
|
|
||||||
settings: {
|
|
||||||
app: 'ontime',
|
|
||||||
version: '2.0.0',
|
|
||||||
},
|
|
||||||
userFields: {
|
|
||||||
notThis: 'this shouldng be accepted',
|
|
||||||
orThis: 'this neither',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const parsed = parseUserFields(testData);
|
|
||||||
expect(parsed).toStrictEqual(model);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('test views import', () => {
|
|
||||||
it('imports data from file', () => {
|
|
||||||
const testData = {
|
|
||||||
rundown: [],
|
|
||||||
settings: {
|
|
||||||
app: 'ontime',
|
|
||||||
version: '2.0.0',
|
|
||||||
},
|
|
||||||
viewSettings: {
|
|
||||||
normalColor: '#ffffffcc',
|
|
||||||
warningColor: '#FFAB33',
|
|
||||||
dangerColor: '#ED3333',
|
|
||||||
endMessage: '',
|
|
||||||
overrideStyles: false,
|
|
||||||
notAthing: true,
|
|
||||||
},
|
|
||||||
views: {
|
|
||||||
overrideStyles: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const expectedParsedViewSettings = {
|
|
||||||
normalColor: '#ffffffcc',
|
|
||||||
warningColor: '#FFAB33',
|
|
||||||
dangerColor: '#ED3333',
|
|
||||||
endMessage: '',
|
|
||||||
overrideStyles: false,
|
|
||||||
};
|
|
||||||
const parsed = parseViewSettings(testData);
|
|
||||||
expect(parsed).toStrictEqual(expectedParsedViewSettings);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('imports defaults to model', () => {
|
|
||||||
const testData = {
|
|
||||||
rundown: [],
|
|
||||||
settings: {
|
|
||||||
app: 'ontime',
|
|
||||||
version: '2.0.0',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const parsed = parseViewSettings(testData);
|
|
||||||
expect(parsed).toStrictEqual({});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.todo('imports events and blocks, ignores otherwise', () => {});
|
||||||
});
|
});
|
||||||
|
|||||||
+72
-119
@@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
generateId,
|
generateId,
|
||||||
isExcelImportMap,
|
isImportMap,
|
||||||
type ExcelImportMap,
|
type ImportMap,
|
||||||
defaultExcelImportMap,
|
defaultImportMap,
|
||||||
validateEndAction,
|
validateEndAction,
|
||||||
validateTimerType,
|
validateTimerType,
|
||||||
type ExcelImportOptions,
|
type ImportOptions,
|
||||||
validateTimes,
|
validateTimes,
|
||||||
isKnownTimerType,
|
isKnownTimerType,
|
||||||
validateLinkStart,
|
validateLinkStart,
|
||||||
@@ -15,10 +15,11 @@ import {
|
|||||||
OntimeEvent,
|
OntimeEvent,
|
||||||
OntimeRundown,
|
OntimeRundown,
|
||||||
SupportedEvent,
|
SupportedEvent,
|
||||||
UserFields,
|
|
||||||
EndAction,
|
EndAction,
|
||||||
TimerType,
|
TimerType,
|
||||||
TimeStrategy,
|
TimeStrategy,
|
||||||
|
CustomFields,
|
||||||
|
EventCustomFields,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
@@ -35,7 +36,6 @@ import {
|
|||||||
parseHttp,
|
parseHttp,
|
||||||
parseRundown,
|
parseRundown,
|
||||||
parseSettings,
|
parseSettings,
|
||||||
parseUserFields,
|
|
||||||
parseViewSettings,
|
parseViewSettings,
|
||||||
parseCustomFields,
|
parseCustomFields,
|
||||||
} from './parserFunctions.js';
|
} from './parserFunctions.js';
|
||||||
@@ -46,34 +46,46 @@ import { coerceBoolean } from './coerceType.js';
|
|||||||
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||||
export const JSON_MIME = 'application/json';
|
export const JSON_MIME = 'application/json';
|
||||||
|
|
||||||
type ExcelData = Pick<DatabaseModel, 'rundown' | 'userFields'> & {
|
type ExcelData = Pick<DatabaseModel, 'rundown' | 'customFields'> & {
|
||||||
rundownMetadata: Record<string, { row: number; col: number }>;
|
rundownMetadata: Record<string, { row: number; col: number }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function getCustomFieldData(importMap: ImportMap): {
|
||||||
|
customFields: CustomFields;
|
||||||
|
customFieldImportKeys: Record<keyof CustomFields, string>;
|
||||||
|
} {
|
||||||
|
const customFields = {};
|
||||||
|
const customFieldImportKeys = {};
|
||||||
|
for (const key in importMap.custom) {
|
||||||
|
const ontimeName = key;
|
||||||
|
const importName = importMap.custom[key];
|
||||||
|
customFields[ontimeName] = {
|
||||||
|
type: 'string',
|
||||||
|
colour: '',
|
||||||
|
label: ontimeName,
|
||||||
|
};
|
||||||
|
customFieldImportKeys[importName] = ontimeName;
|
||||||
|
}
|
||||||
|
return { customFields, customFieldImportKeys };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Excel array parser
|
* @description Excel array parser
|
||||||
* @param {array} excelData - array with excel sheet
|
* @param {array} excelData - array with excel sheet
|
||||||
* @param {ExcelImportOptions} options - an object that contains the import map
|
* @param {ImportOptions} options - an object that contains the import map
|
||||||
* @returns {object} - parsed object
|
* @returns {object} - parsed object
|
||||||
*/
|
*/
|
||||||
export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImportMap>): ExcelData => {
|
export const parseExcel = (excelData: unknown[][], options?: Partial<ImportMap>): ExcelData => {
|
||||||
const rundownMetadata = {};
|
const rundownMetadata = {};
|
||||||
const importMap: ExcelImportMap = { ...defaultExcelImportMap, ...options };
|
const importMap: ImportMap = { ...defaultImportMap, ...options };
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(importMap)) {
|
for (const [key, value] of Object.entries(importMap)) {
|
||||||
importMap[key] = value.toLocaleLowerCase();
|
if (typeof value === 'string') {
|
||||||
|
importMap[key] = value.toLocaleLowerCase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const customUserFields: UserFields = {
|
|
||||||
user0: importMap.user0,
|
const { customFields, customFieldImportKeys } = getCustomFieldData(importMap);
|
||||||
user1: importMap.user1,
|
|
||||||
user2: importMap.user2,
|
|
||||||
user3: importMap.user3,
|
|
||||||
user4: importMap.user4,
|
|
||||||
user5: importMap.user5,
|
|
||||||
user6: importMap.user6,
|
|
||||||
user7: importMap.user7,
|
|
||||||
user8: importMap.user8,
|
|
||||||
user9: importMap.user9,
|
|
||||||
};
|
|
||||||
const rundown: OntimeRundown = [];
|
const rundown: OntimeRundown = [];
|
||||||
|
|
||||||
// title stuff: strings
|
// title stuff: strings
|
||||||
@@ -99,23 +111,15 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
|||||||
let endActionIndex: number | null = null;
|
let endActionIndex: number | null = null;
|
||||||
let timerTypeIndex: number | null = null;
|
let timerTypeIndex: number | null = null;
|
||||||
|
|
||||||
// user fields: strings
|
// record of column index and the name of the field
|
||||||
let user0Index: number | null = null;
|
const customFieldIndexes: Record<number, string> = {};
|
||||||
let user1Index: number | null = null;
|
|
||||||
let user2Index: number | null = null;
|
|
||||||
let user3Index: number | null = null;
|
|
||||||
let user4Index: number | null = null;
|
|
||||||
let user5Index: number | null = null;
|
|
||||||
let user6Index: number | null = null;
|
|
||||||
let user7Index: number | null = null;
|
|
||||||
let user8Index: number | null = null;
|
|
||||||
let user9Index: number | null = null;
|
|
||||||
|
|
||||||
excelData.forEach((row, rowIndex) => {
|
excelData.forEach((row, rowIndex) => {
|
||||||
if (row.length === 0) {
|
if (row.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: extract generating handlers from importMap
|
||||||
const handlers = {
|
const handlers = {
|
||||||
[importMap.timeStart]: (row: number, col: number) => {
|
[importMap.timeStart]: (row: number, col: number) => {
|
||||||
timeStartIndex = col;
|
timeStartIndex = col;
|
||||||
@@ -162,7 +166,6 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
|||||||
colourIndex = col;
|
colourIndex = col;
|
||||||
rundownMetadata['colour'] = { row, col };
|
rundownMetadata['colour'] = { row, col };
|
||||||
},
|
},
|
||||||
|
|
||||||
[importMap.endAction]: (row: number, col: number) => {
|
[importMap.endAction]: (row: number, col: number) => {
|
||||||
endActionIndex = col;
|
endActionIndex = col;
|
||||||
rundownMetadata['endAction'] = { row, col };
|
rundownMetadata['endAction'] = { row, col };
|
||||||
@@ -179,50 +182,15 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
|||||||
timeDangerIndex = col;
|
timeDangerIndex = col;
|
||||||
rundownMetadata['timeDangerIndex'] = { row, col };
|
rundownMetadata['timeDangerIndex'] = { row, col };
|
||||||
},
|
},
|
||||||
|
custom: (row: number, col: number, columnText: string) => {
|
||||||
[importMap.user0]: (row: number, col: number) => {
|
customFieldIndexes[col] = columnText;
|
||||||
user0Index = col;
|
rundownMetadata[`custom-${columnText}`] = { row, col };
|
||||||
rundownMetadata['user0'] = { row, col };
|
|
||||||
},
|
|
||||||
[importMap.user1]: (row: number, col: number) => {
|
|
||||||
user1Index = col;
|
|
||||||
rundownMetadata['user1'] = { row, col };
|
|
||||||
},
|
|
||||||
[importMap.user2]: (row: number, col: number) => {
|
|
||||||
user2Index = col;
|
|
||||||
rundownMetadata['user2'] = { row, col };
|
|
||||||
},
|
|
||||||
[importMap.user3]: (row: number, col: number) => {
|
|
||||||
user3Index = col;
|
|
||||||
rundownMetadata['user3'] = { row, col };
|
|
||||||
},
|
|
||||||
[importMap.user4]: (row: number, col: number) => {
|
|
||||||
user4Index = col;
|
|
||||||
rundownMetadata['user4'] = { row, col };
|
|
||||||
},
|
|
||||||
[importMap.user5]: (row: number, col: number) => {
|
|
||||||
user5Index = col;
|
|
||||||
rundownMetadata['user5'] = { row, col };
|
|
||||||
},
|
|
||||||
[importMap.user6]: (row: number, col: number) => {
|
|
||||||
user6Index = col;
|
|
||||||
rundownMetadata['user6'] = { row, col };
|
|
||||||
},
|
|
||||||
[importMap.user7]: (row: number, col: number) => {
|
|
||||||
user7Index = col;
|
|
||||||
rundownMetadata['user7'] = { row, col };
|
|
||||||
},
|
|
||||||
[importMap.user8]: (row: number, col: number) => {
|
|
||||||
user8Index = col;
|
|
||||||
rundownMetadata['user8'] = { row, col };
|
|
||||||
},
|
|
||||||
[importMap.user9]: (row: number, col: number) => {
|
|
||||||
user9Index = col;
|
|
||||||
rundownMetadata['user9'] = { row, col };
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const event: any = {};
|
const event: any = {};
|
||||||
|
const eventCustomFields: EventCustomFields = {};
|
||||||
|
|
||||||
row.forEach((column, j) => {
|
row.forEach((column, j) => {
|
||||||
// 1. we check if we have set a flag for a known field
|
// 1. we check if we have set a flag for a known field
|
||||||
if (j === timerTypeIndex) {
|
if (j === timerTypeIndex) {
|
||||||
@@ -267,49 +235,45 @@ export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImport
|
|||||||
event.timeDanger = parseExcelDate(column);
|
event.timeDanger = parseExcelDate(column);
|
||||||
} else if (j === colourIndex) {
|
} else if (j === colourIndex) {
|
||||||
event.colour = makeString(column, '');
|
event.colour = makeString(column, '');
|
||||||
} else if (j === user0Index) {
|
} else if (j in customFieldIndexes) {
|
||||||
event.user0 = makeString(column, '');
|
const importKey = customFieldIndexes[j];
|
||||||
} else if (j === user1Index) {
|
const ontimeKey = customFieldImportKeys[importKey];
|
||||||
event.user1 = makeString(column, '');
|
eventCustomFields[ontimeKey] = { value: makeString(column, '') };
|
||||||
} else if (j === user2Index) {
|
|
||||||
event.user2 = makeString(column, '');
|
|
||||||
} else if (j === user3Index) {
|
|
||||||
event.user3 = makeString(column, '');
|
|
||||||
} else if (j === user4Index) {
|
|
||||||
event.user4 = makeString(column, '');
|
|
||||||
} else if (j === user5Index) {
|
|
||||||
event.user5 = makeString(column, '');
|
|
||||||
} else if (j === user6Index) {
|
|
||||||
event.user6 = makeString(column, '');
|
|
||||||
} else if (j === user7Index) {
|
|
||||||
event.user7 = makeString(column, '');
|
|
||||||
} else if (j === user8Index) {
|
|
||||||
event.user8 = makeString(column, '');
|
|
||||||
} else if (j === user9Index) {
|
|
||||||
event.user9 = makeString(column, '');
|
|
||||||
} else {
|
} else {
|
||||||
// 2. if there is no flag, lets see if we know the field type
|
// 2. if there is no flag, lets see if we know the field type
|
||||||
if (typeof column === 'string') {
|
if (typeof column === 'string') {
|
||||||
const col = column.toLowerCase();
|
// we cant deal with empty content
|
||||||
|
if (column.length === 0) {
|
||||||
if (handlers[col]) {
|
return;
|
||||||
handlers[col](rowIndex, j);
|
|
||||||
}
|
}
|
||||||
|
const columnText = column.toLowerCase();
|
||||||
|
|
||||||
|
// check if it is an ontime column
|
||||||
|
if (handlers[columnText]) {
|
||||||
|
handlers[columnText](rowIndex, j, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it is a custom field
|
||||||
|
if (columnText in customFieldImportKeys) {
|
||||||
|
handlers.custom(rowIndex, j, columnText);
|
||||||
|
}
|
||||||
|
|
||||||
// else. we don't know how to handle this column
|
// else. we don't know how to handle this column
|
||||||
// just ignore it
|
// just ignore it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Object.keys(event).length > 0) {
|
// if any data was found in row, push to array
|
||||||
// if any data was found, push to array
|
const keysFound = Object.keys(event).length + Object.keys(eventCustomFields).length;
|
||||||
rundown.push({ ...event });
|
if (keysFound > 0) {
|
||||||
|
rundown.push({ ...event, custom: { ...eventCustomFields } });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rundown,
|
rundown,
|
||||||
userFields: customUserFields,
|
customFields,
|
||||||
rundownMetadata,
|
rundownMetadata,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -330,7 +294,6 @@ export const parseJson = async (jsonData: Partial<DatabaseModel>): Promise<Datab
|
|||||||
settings: parseSettings(jsonData) ?? dbModel.settings,
|
settings: parseSettings(jsonData) ?? dbModel.settings,
|
||||||
viewSettings: parseViewSettings(jsonData) ?? dbModel.viewSettings,
|
viewSettings: parseViewSettings(jsonData) ?? dbModel.viewSettings,
|
||||||
aliases: parseAliases(jsonData),
|
aliases: parseAliases(jsonData),
|
||||||
userFields: parseUserFields(jsonData),
|
|
||||||
customFields: parseCustomFields(jsonData),
|
customFields: parseCustomFields(jsonData),
|
||||||
osc: parseOsc(jsonData) ?? dbModel.osc,
|
osc: parseOsc(jsonData) ?? dbModel.osc,
|
||||||
http: parseHttp(jsonData) ?? dbModel.http,
|
http: parseHttp(jsonData) ?? dbModel.http,
|
||||||
@@ -386,16 +349,6 @@ export function createPatch(originalEvent: OntimeEvent, patchEvent: Partial<Onti
|
|||||||
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
|
isPublic: typeof patchEvent.isPublic === 'boolean' ? patchEvent.isPublic : originalEvent.isPublic,
|
||||||
skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip,
|
skip: typeof patchEvent.skip === 'boolean' ? patchEvent.skip : originalEvent.skip,
|
||||||
note: makeString(patchEvent.note, originalEvent.note),
|
note: makeString(patchEvent.note, originalEvent.note),
|
||||||
user0: makeString(patchEvent.user0, originalEvent.user0),
|
|
||||||
user1: makeString(patchEvent.user1, originalEvent.user1),
|
|
||||||
user2: makeString(patchEvent.user2, originalEvent.user2),
|
|
||||||
user3: makeString(patchEvent.user3, originalEvent.user3),
|
|
||||||
user4: makeString(patchEvent.user4, originalEvent.user4),
|
|
||||||
user5: makeString(patchEvent.user5, originalEvent.user5),
|
|
||||||
user6: makeString(patchEvent.user6, originalEvent.user6),
|
|
||||||
user7: makeString(patchEvent.user7, originalEvent.user7),
|
|
||||||
user8: makeString(patchEvent.user8, originalEvent.user8),
|
|
||||||
user9: makeString(patchEvent.user9, originalEvent.user9),
|
|
||||||
colour: makeString(patchEvent.colour, originalEvent.colour),
|
colour: makeString(patchEvent.colour, originalEvent.colour),
|
||||||
// short circuit empty string
|
// short circuit empty string
|
||||||
cue: makeString(patchEvent.cue ?? null, originalEvent.cue),
|
cue: makeString(patchEvent.cue ?? null, originalEvent.cue),
|
||||||
@@ -436,7 +389,7 @@ type ResponseOK = {
|
|||||||
* @param options - import options
|
* @param options - import options
|
||||||
* @return {object} - parse result message
|
* @return {object} - parse result message
|
||||||
*/
|
*/
|
||||||
export const fileHandler = async (file: string, options: ExcelImportOptions): Promise<Partial<ResponseOK>> => {
|
export const fileHandler = async (file: string, options: ImportOptions): Promise<Partial<ResponseOK>> => {
|
||||||
const res: Partial<ResponseOK> = {};
|
const res: Partial<ResponseOK> = {};
|
||||||
|
|
||||||
const fileName = path.basename(file);
|
const fileName = path.basename(file);
|
||||||
@@ -444,8 +397,8 @@ export const fileHandler = async (file: string, options: ExcelImportOptions): Pr
|
|||||||
// check which file type are we dealing with
|
// check which file type are we dealing with
|
||||||
if (file.endsWith('.xlsx')) {
|
if (file.endsWith('.xlsx')) {
|
||||||
// we need to check that the options are applicable
|
// we need to check that the options are applicable
|
||||||
if (!isExcelImportMap(options)) {
|
if (!isImportMap(options)) {
|
||||||
throw new Error('Got incorrect options to excel import');
|
throw new Error('Got incorrect options for spreadsheet import');
|
||||||
}
|
}
|
||||||
|
|
||||||
const excelData = xlsx
|
const excelData = xlsx
|
||||||
@@ -461,9 +414,9 @@ export const fileHandler = async (file: string, options: ExcelImportOptions): Pr
|
|||||||
res.data = {};
|
res.data = {};
|
||||||
res.data.rundown = parseRundown(dataFromExcel);
|
res.data.rundown = parseRundown(dataFromExcel);
|
||||||
if (res.data.rundown.length < 1) {
|
if (res.data.rundown.length < 1) {
|
||||||
throw new Error(`Could not find data to import in the worksheet ${options.worksheet}`);
|
throw new Error(`Could not find data to import in the worksheet: ${options.worksheet}`);
|
||||||
}
|
}
|
||||||
res.data.userFields = parseUserFields(dataFromExcel);
|
res.data.customFields = parseCustomFields(dataFromExcel);
|
||||||
|
|
||||||
deleteFile(file);
|
deleteFile(file);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
OSCSettings,
|
OSCSettings,
|
||||||
ProjectData,
|
ProjectData,
|
||||||
Settings,
|
Settings,
|
||||||
UserFields,
|
|
||||||
ViewSettings,
|
ViewSettings,
|
||||||
OscSubscription,
|
OscSubscription,
|
||||||
DatabaseModel,
|
DatabaseModel,
|
||||||
@@ -251,33 +250,6 @@ export const parseAliases = (data): Alias[] => {
|
|||||||
return newAliases;
|
return newAliases;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse userFields entry
|
|
||||||
* @param {object} data - data object
|
|
||||||
* @returns {object} - event object data
|
|
||||||
*/
|
|
||||||
export const parseUserFields = (data): UserFields => {
|
|
||||||
const newUserFields: UserFields = { ...dbModel.userFields };
|
|
||||||
|
|
||||||
if ('userFields' in data) {
|
|
||||||
console.log('Found User Fields definition, importing...');
|
|
||||||
// we will only be importing the fields we know, so look for that
|
|
||||||
try {
|
|
||||||
let fieldsFound = 0;
|
|
||||||
for (const n in newUserFields) {
|
|
||||||
if (n in data.userFields) {
|
|
||||||
fieldsFound++;
|
|
||||||
newUserFields[n] = data.userFields[n];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(`Uploaded ${fieldsFound} user fields`);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { ...newUserFields };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse customFields entry
|
* Parse customFields entry
|
||||||
* @param {object} data - data object
|
* @param {object} data - data object
|
||||||
|
|||||||
+26
-165
@@ -13,20 +13,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "32d31",
|
"id": "32d31",
|
||||||
"cue": "SF1.01"
|
"cue": "SF1.01",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Latvia",
|
"title": "Latvia",
|
||||||
@@ -41,20 +32,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "21cd2",
|
"id": "21cd2",
|
||||||
"cue": "SF1.02"
|
"cue": "SF1.02",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Lithuania",
|
"title": "Lithuania",
|
||||||
@@ -69,16 +51,6 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "0b371",
|
"id": "0b371",
|
||||||
@@ -97,20 +69,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "3cd28",
|
"id": "3cd28",
|
||||||
"cue": "SF1.04"
|
"cue": "SF1.04",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Slovenia",
|
"title": "Slovenia",
|
||||||
@@ -125,20 +88,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "e457f",
|
"id": "e457f",
|
||||||
"cue": "SF1.05"
|
"cue": "SF1.05",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Lunch break",
|
"title": "Lunch break",
|
||||||
@@ -158,20 +112,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "1c420",
|
"id": "1c420",
|
||||||
"cue": "SF1.06"
|
"cue": "SF1.06",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Bulgaria",
|
"title": "Bulgaria",
|
||||||
@@ -186,20 +131,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "b7737",
|
"id": "b7737",
|
||||||
"cue": "SF1.07"
|
"cue": "SF1.07",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Netherlands",
|
"title": "Netherlands",
|
||||||
@@ -214,20 +150,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "d3a80",
|
"id": "d3a80",
|
||||||
"cue": "SF1.08"
|
"cue": "SF1.08",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Moldova",
|
"title": "Moldova",
|
||||||
@@ -242,20 +169,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "8276c",
|
"id": "8276c",
|
||||||
"cue": "SF1.09"
|
"cue": "SF1.09",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Portugal",
|
"title": "Portugal",
|
||||||
@@ -270,20 +188,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "2340b",
|
"id": "2340b",
|
||||||
"cue": "SF1.10"
|
"cue": "SF1.10",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Afternoon break",
|
"title": "Afternoon break",
|
||||||
@@ -303,20 +212,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "503c4",
|
"id": "503c4",
|
||||||
"cue": "SF1.11"
|
"cue": "SF1.11",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Denmark",
|
"title": "Denmark",
|
||||||
@@ -331,20 +231,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "5e965",
|
"id": "5e965",
|
||||||
"cue": "SF1.12"
|
"cue": "SF1.12",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Austria",
|
"title": "Austria",
|
||||||
@@ -359,20 +250,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "bab4a",
|
"id": "bab4a",
|
||||||
"cue": "SF1.13"
|
"cue": "SF1.13",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Greece",
|
"title": "Greece",
|
||||||
@@ -387,20 +269,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "d3eb1",
|
"id": "d3eb1",
|
||||||
"cue": "SF1.14"
|
"cue": "SF1.14",
|
||||||
|
"custom": {}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"project": {
|
"project": {
|
||||||
@@ -436,18 +309,6 @@
|
|||||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"userFields": {
|
|
||||||
"user0": "user0",
|
|
||||||
"user1": "user1",
|
|
||||||
"user2": "user2",
|
|
||||||
"user3": "user3",
|
|
||||||
"user4": "user4",
|
|
||||||
"user5": "user5",
|
|
||||||
"user6": "user6",
|
|
||||||
"user7": "user7",
|
|
||||||
"user8": "user8",
|
|
||||||
"user9": "user9"
|
|
||||||
},
|
|
||||||
"osc": {
|
"osc": {
|
||||||
"portIn": 8888,
|
"portIn": 8888,
|
||||||
"portOut": 9999,
|
"portOut": 9999,
|
||||||
|
|||||||
+28
-166
@@ -13,20 +13,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "32d31",
|
"id": "32d31",
|
||||||
"cue": "SF1.01"
|
"cue": "SF1.01",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Latvia",
|
"title": "Latvia",
|
||||||
@@ -41,20 +32,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "21cd2",
|
"id": "21cd2",
|
||||||
"cue": "SF1.02"
|
"cue": "SF1.02",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Lithuania",
|
"title": "Lithuania",
|
||||||
@@ -69,20 +51,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "0b371",
|
"id": "0b371",
|
||||||
"cue": "SF1.03"
|
"cue": "SF1.03",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Switzerland",
|
"title": "Switzerland",
|
||||||
@@ -97,20 +70,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "3cd28",
|
"id": "3cd28",
|
||||||
"cue": "SF1.04"
|
"cue": "SF1.04",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Slovenia",
|
"title": "Slovenia",
|
||||||
@@ -125,20 +89,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "e457f",
|
"id": "e457f",
|
||||||
"cue": "SF1.05"
|
"cue": "SF1.05",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Lunch break",
|
"title": "Lunch break",
|
||||||
@@ -158,20 +113,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "1c420",
|
"id": "1c420",
|
||||||
"cue": "SF1.06"
|
"cue": "SF1.06",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Bulgaria",
|
"title": "Bulgaria",
|
||||||
@@ -186,20 +132,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "b7737",
|
"id": "b7737",
|
||||||
"cue": "SF1.07"
|
"cue": "SF1.07",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Netherlands",
|
"title": "Netherlands",
|
||||||
@@ -214,20 +151,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "d3a80",
|
"id": "d3a80",
|
||||||
"cue": "SF1.08"
|
"cue": "SF1.08",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Moldova",
|
"title": "Moldova",
|
||||||
@@ -242,20 +170,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "8276c",
|
"id": "8276c",
|
||||||
"cue": "SF1.09"
|
"cue": "SF1.09",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Portugal",
|
"title": "Portugal",
|
||||||
@@ -270,20 +189,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "2340b",
|
"id": "2340b",
|
||||||
"cue": "SF1.10"
|
"cue": "SF1.10",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Afternoon break",
|
"title": "Afternoon break",
|
||||||
@@ -303,20 +213,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "503c4",
|
"id": "503c4",
|
||||||
"cue": "SF1.11"
|
"cue": "SF1.11",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Denmark",
|
"title": "Denmark",
|
||||||
@@ -331,20 +232,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "5e965",
|
"id": "5e965",
|
||||||
"cue": "SF1.12"
|
"cue": "SF1.12",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Austria",
|
"title": "Austria",
|
||||||
@@ -359,20 +251,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "bab4a",
|
"id": "bab4a",
|
||||||
"cue": "SF1.13"
|
"cue": "SF1.13",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Greece",
|
"title": "Greece",
|
||||||
@@ -387,20 +270,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "d3eb1",
|
"id": "d3eb1",
|
||||||
"cue": "SF1.14"
|
"cue": "SF1.14",
|
||||||
|
"custom": {}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"project": {
|
"project": {
|
||||||
@@ -436,18 +310,6 @@
|
|||||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"userFields": {
|
|
||||||
"user0": "user0",
|
|
||||||
"user1": "user1",
|
|
||||||
"user2": "user2",
|
|
||||||
"user3": "user3",
|
|
||||||
"user4": "user4",
|
|
||||||
"user5": "user5",
|
|
||||||
"user6": "user6",
|
|
||||||
"user7": "user7",
|
|
||||||
"user8": "user8",
|
|
||||||
"user9": "user9"
|
|
||||||
},
|
|
||||||
"osc": {
|
"osc": {
|
||||||
"portIn": 8888,
|
"portIn": 8888,
|
||||||
"portOut": 9999,
|
"portOut": 9999,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ test('test project file upload', async ({ page }) => {
|
|||||||
// https://playwright.dev/docs/api/class-filechooser
|
// https://playwright.dev/docs/api/class-filechooser
|
||||||
const [fileChooser] = await Promise.all([
|
const [fileChooser] = await Promise.all([
|
||||||
page.waitForEvent('filechooser'),
|
page.waitForEvent('filechooser'),
|
||||||
await page.getByText('Click to select Ontime project or xlsx rundown').click(),
|
await page.getByText('Click to select Ontime project').click(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await fileChooser.setFiles(fileToUpload);
|
await fileChooser.setFiles(fileToUpload);
|
||||||
|
|||||||
Vendored
+28
-166
@@ -13,20 +13,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "32d31",
|
"id": "32d31",
|
||||||
"cue": "SF1.01"
|
"cue": "SF1.01",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Latvia",
|
"title": "Latvia",
|
||||||
@@ -41,20 +32,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "21cd2",
|
"id": "21cd2",
|
||||||
"cue": "SF1.02"
|
"cue": "SF1.02",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Lithuania",
|
"title": "Lithuania",
|
||||||
@@ -69,20 +51,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "0b371",
|
"id": "0b371",
|
||||||
"cue": "SF1.03"
|
"cue": "SF1.03",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Switzerland",
|
"title": "Switzerland",
|
||||||
@@ -97,20 +70,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "3cd28",
|
"id": "3cd28",
|
||||||
"cue": "SF1.04"
|
"cue": "SF1.04",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Slovenia",
|
"title": "Slovenia",
|
||||||
@@ -125,20 +89,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "e457f",
|
"id": "e457f",
|
||||||
"cue": "SF1.05"
|
"cue": "SF1.05",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Lunch break",
|
"title": "Lunch break",
|
||||||
@@ -158,20 +113,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "1c420",
|
"id": "1c420",
|
||||||
"cue": "SF1.06"
|
"cue": "SF1.06",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Bulgaria",
|
"title": "Bulgaria",
|
||||||
@@ -186,20 +132,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "b7737",
|
"id": "b7737",
|
||||||
"cue": "SF1.07"
|
"cue": "SF1.07",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Netherlands",
|
"title": "Netherlands",
|
||||||
@@ -214,20 +151,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "d3a80",
|
"id": "d3a80",
|
||||||
"cue": "SF1.08"
|
"cue": "SF1.08",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Moldova",
|
"title": "Moldova",
|
||||||
@@ -242,20 +170,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "8276c",
|
"id": "8276c",
|
||||||
"cue": "SF1.09"
|
"cue": "SF1.09",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Portugal",
|
"title": "Portugal",
|
||||||
@@ -270,20 +189,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "2340b",
|
"id": "2340b",
|
||||||
"cue": "SF1.10"
|
"cue": "SF1.10",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Afternoon break",
|
"title": "Afternoon break",
|
||||||
@@ -303,20 +213,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "503c4",
|
"id": "503c4",
|
||||||
"cue": "SF1.11"
|
"cue": "SF1.11",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Denmark",
|
"title": "Denmark",
|
||||||
@@ -331,20 +232,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "5e965",
|
"id": "5e965",
|
||||||
"cue": "SF1.12"
|
"cue": "SF1.12",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Austria",
|
"title": "Austria",
|
||||||
@@ -359,20 +251,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "bab4a",
|
"id": "bab4a",
|
||||||
"cue": "SF1.13"
|
"cue": "SF1.13",
|
||||||
|
"custom": {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Greece",
|
"title": "Greece",
|
||||||
@@ -387,20 +270,11 @@
|
|||||||
"isPublic": true,
|
"isPublic": true,
|
||||||
"skip": false,
|
"skip": false,
|
||||||
"colour": "",
|
"colour": "",
|
||||||
"user0": "",
|
|
||||||
"user1": "",
|
|
||||||
"user2": "",
|
|
||||||
"user3": "",
|
|
||||||
"user4": "",
|
|
||||||
"user5": "",
|
|
||||||
"user6": "",
|
|
||||||
"user7": "",
|
|
||||||
"user8": "",
|
|
||||||
"user9": "",
|
|
||||||
"type": "event",
|
"type": "event",
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"id": "d3eb1",
|
"id": "d3eb1",
|
||||||
"cue": "SF1.14"
|
"cue": "SF1.14",
|
||||||
|
"custom": {}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"project": {
|
"project": {
|
||||||
@@ -436,18 +310,6 @@
|
|||||||
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
"pathAndParams": "lower?bg=ff2&text=f00&size=0.6&transition=5"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"userFields": {
|
|
||||||
"user0": "user0",
|
|
||||||
"user1": "user1",
|
|
||||||
"user2": "user2",
|
|
||||||
"user3": "user3",
|
|
||||||
"user4": "user4",
|
|
||||||
"user5": "user5",
|
|
||||||
"user6": "user6",
|
|
||||||
"user7": "user7",
|
|
||||||
"user8": "user8",
|
|
||||||
"user9": "user9"
|
|
||||||
},
|
|
||||||
"osc": {
|
"osc": {
|
||||||
"portIn": 8888,
|
"portIn": 8888,
|
||||||
"portOut": 9999,
|
"portOut": 9999,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { ProjectData } from './core/ProjectData.type.js';
|
|||||||
import { OntimeRundown } from './core/Rundown.type.js';
|
import { OntimeRundown } from './core/Rundown.type.js';
|
||||||
import { OSCSettings } from './core/OscSettings.type.js';
|
import { OSCSettings } from './core/OscSettings.type.js';
|
||||||
import { Settings } from './core/Settings.type.js';
|
import { Settings } from './core/Settings.type.js';
|
||||||
import { UserFields } from './core/UserFields.type.js';
|
|
||||||
import { ViewSettings } from './core/Views.type.js';
|
import { ViewSettings } from './core/Views.type.js';
|
||||||
import { CustomFields, HttpSettings } from '../index.js';
|
import { CustomFields, HttpSettings } from '../index.js';
|
||||||
|
|
||||||
@@ -13,7 +12,6 @@ export type DatabaseModel = {
|
|||||||
settings: Settings;
|
settings: Settings;
|
||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
aliases: Alias[];
|
aliases: Alias[];
|
||||||
userFields: UserFields;
|
|
||||||
customFields: CustomFields;
|
customFields: CustomFields;
|
||||||
osc: OSCSettings;
|
osc: OSCSettings;
|
||||||
http: HttpSettings;
|
http: HttpSettings;
|
||||||
|
|||||||
@@ -39,16 +39,6 @@ export type OntimeEvent = OntimeBaseEvent & {
|
|||||||
isPublic: boolean;
|
isPublic: boolean;
|
||||||
skip: boolean;
|
skip: boolean;
|
||||||
colour: string;
|
colour: string;
|
||||||
user0: string;
|
|
||||||
user1: string;
|
|
||||||
user2: string;
|
|
||||||
user3: string;
|
|
||||||
user4: string;
|
|
||||||
user5: string;
|
|
||||||
user6: string;
|
|
||||||
user7: string;
|
|
||||||
user8: string;
|
|
||||||
user9: string;
|
|
||||||
revision: number;
|
revision: number;
|
||||||
delay?: number; // calculated at runtime
|
delay?: number; // calculated at runtime
|
||||||
timeWarning: number;
|
timeWarning: number;
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
export type UserFields = {
|
|
||||||
user0: string;
|
|
||||||
user1: string;
|
|
||||||
user2: string;
|
|
||||||
user3: string;
|
|
||||||
user4: string;
|
|
||||||
user5: string;
|
|
||||||
user6: string;
|
|
||||||
user7: string;
|
|
||||||
user8: string;
|
|
||||||
user9: string;
|
|
||||||
};
|
|
||||||
@@ -27,9 +27,6 @@ export type { TimeFormat } from './definitions/core/TimeFormat.type.js';
|
|||||||
// ---> Aliases
|
// ---> Aliases
|
||||||
export type { Alias } from './definitions/core/Alias.type.js';
|
export type { Alias } from './definitions/core/Alias.type.js';
|
||||||
|
|
||||||
// ---> User Fields
|
|
||||||
export type { UserFields } from './definitions/core/UserFields.type.js';
|
|
||||||
|
|
||||||
// ---> Custom Fields
|
// ---> Custom Fields
|
||||||
export type {
|
export type {
|
||||||
CustomFields,
|
CustomFields,
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export { deepmerge } from './src/externals/deepmerge.js';
|
|||||||
export { deleteAtIndex, insertAtIndex, reorderArray, sortArrayByProperty } from './src/array-utils/arrayUtils.js';
|
export { deleteAtIndex, insertAtIndex, reorderArray, sortArrayByProperty } from './src/array-utils/arrayUtils.js';
|
||||||
|
|
||||||
// generic utilities
|
// generic utilities
|
||||||
|
export { unpackError } from './src/generic/generic.js';
|
||||||
export { isNumeric } from './src/types/types.js';
|
export { isNumeric } from './src/types/types.js';
|
||||||
|
|
||||||
// model validation
|
// model validation
|
||||||
@@ -62,10 +63,11 @@ export { validateEndAction, validateTimerType } from './src/validate-events/vali
|
|||||||
|
|
||||||
// feature business logic
|
// feature business logic
|
||||||
|
|
||||||
// feature business logic - excel import
|
// feature business logic - spreadsheet import
|
||||||
export {
|
export {
|
||||||
type ExcelImportMap,
|
type ImportCustom,
|
||||||
type ExcelImportOptions,
|
type ImportMap,
|
||||||
defaultExcelImportMap,
|
type ImportOptions,
|
||||||
isExcelImportMap,
|
defaultImportMap,
|
||||||
} from './src/feature/excel-import/excelImport.js';
|
isImportMap,
|
||||||
|
} from './src/feature/spreadsheet-import/spreadsheetImport.js';
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import { isExcelImportMap } from '../excelImport';
|
|
||||||
|
|
||||||
describe('isExcelImportMap', () => {
|
|
||||||
test('migrate v2 map', () => {
|
|
||||||
const v2ImportMap = {
|
|
||||||
worksheet: 'event schedule',
|
|
||||||
projectName: 'project name',
|
|
||||||
projectDescription: 'project description',
|
|
||||||
publicUrl: 'public url',
|
|
||||||
publicInfo: 'public info',
|
|
||||||
backstageUrl: 'backstage url',
|
|
||||||
backstageInfo: 'backstage info',
|
|
||||||
timeStart: 'time start',
|
|
||||||
timeEnd: 'time end',
|
|
||||||
duration: 'duration',
|
|
||||||
cue: 'cue',
|
|
||||||
title: 'title',
|
|
||||||
presenter: 'presenter',
|
|
||||||
subtitle: 'subtitle',
|
|
||||||
isPublic: 'public',
|
|
||||||
skip: 'skip',
|
|
||||||
note: 'notes',
|
|
||||||
colour: 'colour',
|
|
||||||
endAction: 'end action',
|
|
||||||
timerType: 'timer type',
|
|
||||||
user0: 'header',
|
|
||||||
user1: 'user1',
|
|
||||||
user2: 'user2',
|
|
||||||
user3: 'user3',
|
|
||||||
user4: 'user4',
|
|
||||||
user5: 'user5',
|
|
||||||
user6: 'user6',
|
|
||||||
user7: 'user7',
|
|
||||||
user8: 'user8',
|
|
||||||
user9: 'user9',
|
|
||||||
timeWarning: 'warning time',
|
|
||||||
timeDanger: 'danger time',
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(isExcelImportMap(v2ImportMap)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { isImportMap } from '../spreadsheetImport';
|
||||||
|
|
||||||
|
describe('isImportMap()', () => {
|
||||||
|
it('validates a v3 default import map', () => {
|
||||||
|
const v3ImportMap = {
|
||||||
|
worksheet: 'event schedule',
|
||||||
|
timeStart: 'time start',
|
||||||
|
timeEnd: 'time end',
|
||||||
|
duration: 'duration',
|
||||||
|
cue: 'cue',
|
||||||
|
title: 'title',
|
||||||
|
presenter: 'presenter',
|
||||||
|
subtitle: 'subtitle',
|
||||||
|
isPublic: 'public',
|
||||||
|
skip: 'skip',
|
||||||
|
note: 'notes',
|
||||||
|
colour: 'colour',
|
||||||
|
endAction: 'end action',
|
||||||
|
timerType: 'timer type',
|
||||||
|
timeWarning: 'warning time',
|
||||||
|
timeDanger: 'danger time',
|
||||||
|
custom: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(isImportMap(v3ImportMap)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles custom properties', () => {
|
||||||
|
const v3ImportMap = {
|
||||||
|
worksheet: 'event schedule',
|
||||||
|
timeStart: 'time start',
|
||||||
|
timeEnd: 'time end',
|
||||||
|
duration: 'duration',
|
||||||
|
cue: 'cue',
|
||||||
|
title: 'title',
|
||||||
|
presenter: 'presenter',
|
||||||
|
subtitle: 'subtitle',
|
||||||
|
isPublic: 'public',
|
||||||
|
skip: 'skip',
|
||||||
|
note: 'notes',
|
||||||
|
colour: 'colour',
|
||||||
|
endAction: 'end action',
|
||||||
|
timerType: 'timer type',
|
||||||
|
timeWarning: 'warning time',
|
||||||
|
timeDanger: 'danger time',
|
||||||
|
custom: {
|
||||||
|
userDefined: 'userDefined',
|
||||||
|
anotherOne: 'anotherOne',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(isImportMap(v3ImportMap)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
+12
-15
@@ -1,7 +1,9 @@
|
|||||||
export type ExcelImportOptions = keyof typeof defaultExcelImportMap;
|
export type ImportOptions = keyof typeof defaultImportMap | 'custom';
|
||||||
export type ExcelImportMap = typeof defaultExcelImportMap;
|
export type ImportCustom = Record<string, string>;
|
||||||
|
export type ImportMap = typeof defaultImportMap & { custom: ImportCustom };
|
||||||
|
|
||||||
export const defaultExcelImportMap = {
|
// Record of ontime name and import name
|
||||||
|
export const defaultImportMap = {
|
||||||
worksheet: 'event schedule',
|
worksheet: 'event schedule',
|
||||||
timeStart: 'time start',
|
timeStart: 'time start',
|
||||||
timeEnd: 'time end',
|
timeEnd: 'time end',
|
||||||
@@ -16,25 +18,20 @@ export const defaultExcelImportMap = {
|
|||||||
colour: 'colour',
|
colour: 'colour',
|
||||||
endAction: 'end action',
|
endAction: 'end action',
|
||||||
timerType: 'timer type',
|
timerType: 'timer type',
|
||||||
user0: 'user0',
|
|
||||||
user1: 'user1',
|
|
||||||
user2: 'user2',
|
|
||||||
user3: 'user3',
|
|
||||||
user4: 'user4',
|
|
||||||
user5: 'user5',
|
|
||||||
user6: 'user6',
|
|
||||||
user7: 'user7',
|
|
||||||
user8: 'user8',
|
|
||||||
user9: 'user9',
|
|
||||||
timeWarning: 'warning time',
|
timeWarning: 'warning time',
|
||||||
timeDanger: 'danger time',
|
timeDanger: 'danger time',
|
||||||
|
custom: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isExcelImportMap(obj: unknown): obj is ExcelImportMap {
|
/**
|
||||||
|
* Validates whether an object is an Import Map
|
||||||
|
* @param obj
|
||||||
|
*/
|
||||||
|
export function isImportMap(obj: unknown): obj is ImportMap {
|
||||||
if (typeof obj !== 'object' || obj === null) {
|
if (typeof obj !== 'object' || obj === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = Object.keys(defaultExcelImportMap);
|
const keys = Object.keys(defaultImportMap);
|
||||||
return keys.every((key) => Object.hasOwn(obj, key));
|
return keys.every((key) => Object.hasOwn(obj, key));
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export function unpackError(error: unknown): string {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user