mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 23:19:09 +00:00
Project Manager: Quick Start (#762)
This commit is contained in:
@@ -247,13 +247,6 @@ export async function getLatestVersion(): Promise<HasUpdate> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP POST request to create a new project file with given project data
|
|
||||||
*/
|
|
||||||
export async function postNew(initialData: Partial<ProjectData>) {
|
|
||||||
return axios.post(`${ontimeURL}/new`, initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to get the list of available project files
|
* @description HTTP request to get the list of available project files
|
||||||
*/
|
*/
|
||||||
@@ -382,11 +375,15 @@ export async function deleteProject(filename: string): Promise<MessageResponse>
|
|||||||
/**
|
/**
|
||||||
* @description HTTP request to create a project file
|
* @description HTTP request to create a project file
|
||||||
*/
|
*/
|
||||||
export async function createProject(filename: string): Promise<MessageResponse> {
|
export async function createProject(
|
||||||
|
project: Partial<
|
||||||
|
ProjectData & {
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
): Promise<MessageResponse> {
|
||||||
const url = `${ontimeURL}/project`;
|
const url = `${ontimeURL}/project`;
|
||||||
const decodedUrl = decodeURIComponent(url);
|
const decodedUrl = decodeURIComponent(url);
|
||||||
const res = await axios.post(decodedUrl, {
|
const res = await axios.post(decodedUrl, project);
|
||||||
filename,
|
|
||||||
});
|
|
||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { Button, Input, Textarea } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import style from './ProjectPanel.module.scss';
|
||||||
|
|
||||||
|
export type ProjectCreateFormValues = {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
publicInfo?: string;
|
||||||
|
publicUrl?: string;
|
||||||
|
backstageInfo?: string;
|
||||||
|
backstageUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProjectCreateFormProps {
|
||||||
|
onCancel: () => void;
|
||||||
|
onSubmit: (values: ProjectCreateFormValues) => Promise<void>;
|
||||||
|
submitError: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProjectCreateForm({ onSubmit, onCancel, submitError }: ProjectCreateFormProps) {
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
formState: { isSubmitting, isValid },
|
||||||
|
setFocus,
|
||||||
|
} = useForm<ProjectCreateFormValues>({
|
||||||
|
defaultValues: { title: '' },
|
||||||
|
values: { title: '' },
|
||||||
|
resetOptions: {
|
||||||
|
keepDirtyValues: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus('title');
|
||||||
|
}, [setFocus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className={style.createFormInputField}>
|
||||||
|
<label>
|
||||||
|
Project title
|
||||||
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
maxLength={50}
|
||||||
|
placeholder='Your project name'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('title')}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={style.createFormInputField}>
|
||||||
|
<label>
|
||||||
|
Project description
|
||||||
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
maxLength={100}
|
||||||
|
placeholder='Euro Love, Malmö 2024'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('description')}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={style.createFormInputField}>
|
||||||
|
<label>
|
||||||
|
Public info
|
||||||
|
<Textarea
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
maxLength={150}
|
||||||
|
placeholder='Shows always start ontime'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('publicInfo')}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={style.createFormInputField}>
|
||||||
|
<label>
|
||||||
|
Public QR code Url
|
||||||
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder='www.getontime.no'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('publicUrl')}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={style.createFormInputField}>
|
||||||
|
<label>
|
||||||
|
Backstage info
|
||||||
|
<Textarea
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
maxLength={150}
|
||||||
|
placeholder='Wi-Fi password: 1234'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('backstageInfo')}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={style.createFormInputField}>
|
||||||
|
<label>
|
||||||
|
Backstage QR code Url
|
||||||
|
<Input
|
||||||
|
variant='ontime-filled'
|
||||||
|
size='sm'
|
||||||
|
placeholder='www.ontime.gitbook.io'
|
||||||
|
autoComplete='off'
|
||||||
|
{...register('backstageUrl')}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={style.createActionButtons}>
|
||||||
|
<Button onClick={onCancel} variant='ontime-ghosted' size='sm'>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isDisabled={!isValid}
|
||||||
|
type='submit'
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
variant='ontime-filled'
|
||||||
|
padding='0 2em'
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{submitError && <span className={style.error}>{submitError}</span>}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ export type ProjectFormValues = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface ProjectFormProps {
|
interface ProjectFormProps {
|
||||||
action: 'duplicate' | 'rename' | 'create';
|
action: 'duplicate' | 'rename';
|
||||||
filename: string;
|
filename: string;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSubmit: (values: ProjectFormValues) => Promise<void>;
|
onSubmit: (values: ProjectFormValues) => Promise<void>;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createProject } from '../../../../common/api/ontimeApi';
|
|||||||
import { useProjectList } from '../../../../common/hooks-query/useProjectList';
|
import { useProjectList } from '../../../../common/hooks-query/useProjectList';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
import ProjectCreateForm, { ProjectCreateFormValues } from './ProjectCreateForm';
|
||||||
import ProjectListItem, { EditMode } from './ProjectListItem';
|
import ProjectListItem, { EditMode } from './ProjectListItem';
|
||||||
|
|
||||||
import style from './ProjectPanel.module.scss';
|
import style from './ProjectPanel.module.scss';
|
||||||
@@ -28,15 +28,15 @@ export default function ProjectList({ isCreatingProject, onToggleCreate }: Proje
|
|||||||
setSubmitError(null);
|
setSubmitError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitCreate = async (values: ProjectFormValues) => {
|
const handleSubmitCreate = async (values: ProjectCreateFormValues) => {
|
||||||
try {
|
try {
|
||||||
setSubmitError(null);
|
setSubmitError(null);
|
||||||
const filename = values.filename.trim();
|
const filename = values.title?.trim();
|
||||||
if (!filename) {
|
|
||||||
setSubmitError('Project name cannot be empty');
|
await createProject({
|
||||||
return;
|
...values,
|
||||||
}
|
filename,
|
||||||
await createProject(filename);
|
});
|
||||||
await refetch();
|
await refetch();
|
||||||
handleToggleCreateMode();
|
handleToggleCreateMode();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -82,13 +82,7 @@ export default function ProjectList({ isCreatingProject, onToggleCreate }: Proje
|
|||||||
{isCreatingProject ? (
|
{isCreatingProject ? (
|
||||||
<tr className={style.createContainer}>
|
<tr className={style.createContainer}>
|
||||||
<td colSpan={99}>
|
<td colSpan={99}>
|
||||||
<ProjectForm
|
<ProjectCreateForm onSubmit={handleSubmitCreate} onCancel={handleToggleCreateMode} submitError='' />
|
||||||
action='create'
|
|
||||||
filename=''
|
|
||||||
onSubmit={handleSubmitCreate}
|
|
||||||
onCancel={handleToggleCreateMode}
|
|
||||||
submitError=''
|
|
||||||
/>
|
|
||||||
{submitError && <span className={style.createSubmitError}>{submitError}</span>}
|
{submitError && <span className={style.createSubmitError}>{submitError}</span>}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -19,6 +19,10 @@
|
|||||||
flex: 2;
|
flex: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.createFormInputField {
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.actionButtons {
|
.actionButtons {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -26,6 +30,14 @@
|
|||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.createActionButtons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: $red-500;
|
color: $red-500;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'
|
|||||||
import AppSettings from '../app-settings/AppSettings';
|
import AppSettings from '../app-settings/AppSettings';
|
||||||
import { SettingsOptionId, useSettingsStore } from '../app-settings/settingsStore';
|
import { SettingsOptionId, useSettingsStore } from '../app-settings/settingsStore';
|
||||||
import MenuBar from '../menu/MenuBar';
|
import MenuBar from '../menu/MenuBar';
|
||||||
import QuickStart from '../modals/quick-start/QuickStart';
|
|
||||||
import SheetsModal from '../modals/sheets-modal/SheetsModal';
|
import SheetsModal from '../modals/sheets-modal/SheetsModal';
|
||||||
import UploadModal from '../modals/upload-modal/UploadModal';
|
import UploadModal from '../modals/upload-modal/UploadModal';
|
||||||
import Overview from '../overview/Overview';
|
import Overview from '../overview/Overview';
|
||||||
@@ -33,7 +32,6 @@ export default function Editor() {
|
|||||||
onOpen: onIntegrationModalOpen,
|
onOpen: onIntegrationModalOpen,
|
||||||
onClose: onIntegrationModalClose,
|
onClose: onIntegrationModalClose,
|
||||||
} = useDisclosure();
|
} = useDisclosure();
|
||||||
const { isOpen: isQuickStartOpen, onOpen: onQuickStartOpen, onClose: onQuickStartClose } = useDisclosure();
|
|
||||||
const { isOpen: isSheetsOpen, onOpen: onSheetsOpen, onClose: onSheetsClose } = useDisclosure();
|
const { isOpen: isSheetsOpen, onOpen: onSheetsOpen, onClose: onSheetsClose } = useDisclosure();
|
||||||
|
|
||||||
// Set window title
|
// Set window title
|
||||||
@@ -46,7 +44,6 @@ export default function Editor() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<QuickStart onClose={onQuickStartClose} isOpen={isQuickStartOpen} />
|
|
||||||
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
|
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
|
||||||
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
|
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
|
||||||
<SettingsModal isOpen={isOldSettingsOpen} onClose={onSettingsClose} />
|
<SettingsModal isOpen={isOldSettingsOpen} onClose={onSettingsClose} />
|
||||||
@@ -62,8 +59,6 @@ export default function Editor() {
|
|||||||
onUploadOpen={onUploadModalOpen}
|
onUploadOpen={onUploadModalOpen}
|
||||||
isIntegrationOpen={isIntegrationModalOpen}
|
isIntegrationOpen={isIntegrationModalOpen}
|
||||||
onIntegrationOpen={onIntegrationModalOpen}
|
onIntegrationOpen={onIntegrationModalOpen}
|
||||||
isQuickStartOpen={isQuickStartOpen}
|
|
||||||
onQuickStartOpen={onQuickStartOpen}
|
|
||||||
openSettings={handleSettings}
|
openSettings={handleSettings}
|
||||||
isSettingsOpen={isSettingsOpen}
|
isSettingsOpen={isSettingsOpen}
|
||||||
isSheetsOpen={isSheetsOpen}
|
isSheetsOpen={isSheetsOpen}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { IconButton, MenuButton, Tooltip } from '@chakra-ui/react';
|
|||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
import { IoCloud } from '@react-icons/all-files/io5/IoCloud';
|
import { IoCloud } from '@react-icons/all-files/io5/IoCloud';
|
||||||
import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
|
import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
|
||||||
import { IoColorWand } from '@react-icons/all-files/io5/IoColorWand';
|
|
||||||
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
|
import { IoExtensionPuzzle } from '@react-icons/all-files/io5/IoExtensionPuzzle';
|
||||||
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
|
import { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
|
||||||
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
||||||
@@ -30,8 +29,6 @@ interface MenuBarProps {
|
|||||||
onUploadOpen: () => void;
|
onUploadOpen: () => void;
|
||||||
isIntegrationOpen: boolean;
|
isIntegrationOpen: boolean;
|
||||||
onIntegrationOpen: () => void;
|
onIntegrationOpen: () => void;
|
||||||
isQuickStartOpen: boolean;
|
|
||||||
onQuickStartOpen: () => void;
|
|
||||||
isSheetsOpen: boolean;
|
isSheetsOpen: boolean;
|
||||||
onSheetsOpen: () => void;
|
onSheetsOpen: () => void;
|
||||||
openSettings: (newTab?: string) => void;
|
openSettings: (newTab?: string) => void;
|
||||||
@@ -59,8 +56,6 @@ const MenuBar = (props: MenuBarProps) => {
|
|||||||
onUploadOpen,
|
onUploadOpen,
|
||||||
isIntegrationOpen,
|
isIntegrationOpen,
|
||||||
onIntegrationOpen,
|
onIntegrationOpen,
|
||||||
isQuickStartOpen,
|
|
||||||
onQuickStartOpen,
|
|
||||||
openSettings,
|
openSettings,
|
||||||
isSettingsOpen,
|
isSettingsOpen,
|
||||||
isSheetsOpen,
|
isSheetsOpen,
|
||||||
@@ -112,15 +107,6 @@ const MenuBar = (props: MenuBarProps) => {
|
|||||||
<div className={style.menu}>
|
<div className={style.menu}>
|
||||||
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} />
|
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} />
|
||||||
<div className={style.gap} />
|
<div className={style.gap} />
|
||||||
<TooltipActionBtn
|
|
||||||
{...buttonStyle}
|
|
||||||
isDisabled={appMode === AppMode.Run}
|
|
||||||
icon={<IoColorWand />}
|
|
||||||
className={isQuickStartOpen ? style.open : ''}
|
|
||||||
clickHandler={onQuickStartOpen}
|
|
||||||
tooltip='Quick start'
|
|
||||||
aria-label='Quick start'
|
|
||||||
/>
|
|
||||||
<TooltipActionBtn
|
<TooltipActionBtn
|
||||||
{...buttonStyle}
|
{...buttonStyle}
|
||||||
isDisabled={appMode === AppMode.Run}
|
isDisabled={appMode === AppMode.Run}
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
AlertDescription,
|
|
||||||
AlertIcon,
|
|
||||||
AlertTitle,
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalContent,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
ModalOverlay,
|
|
||||||
Textarea,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import type { ProjectData } from 'ontime-types';
|
|
||||||
|
|
||||||
import { PROJECT_DATA, RUNDOWN } from '../../../common/api/apiConstants';
|
|
||||||
import { postNew } from '../../../common/api/ontimeApi';
|
|
||||||
import useProjectData from '../../../common/hooks-query/useProjectData';
|
|
||||||
import { projectDataPlaceholder } from '../../../common/models/ProjectData';
|
|
||||||
import { ontimeQueryClient } from '../../../common/queryClient';
|
|
||||||
|
|
||||||
import styles from '../Modal.module.scss';
|
|
||||||
|
|
||||||
interface QuickStartProps {
|
|
||||||
onClose: () => void;
|
|
||||||
isOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function QuickStart({ onClose, isOpen }: QuickStartProps) {
|
|
||||||
const { data, status } = useProjectData();
|
|
||||||
const {
|
|
||||||
handleSubmit,
|
|
||||||
register,
|
|
||||||
reset,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
} = useForm({
|
|
||||||
defaultValues: data,
|
|
||||||
resetOptions: {
|
|
||||||
keepDirtyValues: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) reset(data);
|
|
||||||
}, [data, reset]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: Partial<ProjectData>) => {
|
|
||||||
try {
|
|
||||||
await postNew(data);
|
|
||||||
await ontimeQueryClient.invalidateQueries({ queryKey: PROJECT_DATA });
|
|
||||||
await ontimeQueryClient.invalidateQueries({ queryKey: RUNDOWN });
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
} catch (_) {
|
|
||||||
/* WE DO NOT HANDLE ERRORS */
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onReset = () => reset(projectDataPlaceholder);
|
|
||||||
|
|
||||||
const disableButtons = status !== 'success' || isSubmitting;
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
onClose={onClose}
|
|
||||||
isOpen={isOpen}
|
|
||||||
closeOnOverlayClick={false}
|
|
||||||
motionPreset='slideInBottom'
|
|
||||||
size='xl'
|
|
||||||
scrollBehavior='inside'
|
|
||||||
preserveScrollBarGap
|
|
||||||
variant='ontime'
|
|
||||||
>
|
|
||||||
<ModalOverlay />
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader>Ontime quick start</ModalHeader>
|
|
||||||
<ModalCloseButton />
|
|
||||||
<ModalBody className={styles.pad}>
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className={styles.sectionContainer}>
|
|
||||||
<Alert status='info' variant='ontime-on-light-info'>
|
|
||||||
<AlertIcon />
|
|
||||||
<div className={styles.column}>
|
|
||||||
<AlertTitle>Note</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
On submit, application options will be kept but rundown and project data will be reset
|
|
||||||
</AlertDescription>
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
<div className={styles.entryRow}>
|
|
||||||
<label className={styles.sectionTitle}>
|
|
||||||
Project title
|
|
||||||
<Input
|
|
||||||
variant='ontime-filled-on-light'
|
|
||||||
size='sm'
|
|
||||||
maxLength={50}
|
|
||||||
placeholder='Eurovision song contest'
|
|
||||||
{...register('title')}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={styles.entryRow}>
|
|
||||||
<label className={styles.sectionTitle}>
|
|
||||||
Project description
|
|
||||||
<Input
|
|
||||||
variant='ontime-filled-on-light'
|
|
||||||
size='sm'
|
|
||||||
maxLength={100}
|
|
||||||
placeholder='Euro Love, Malmö 2024'
|
|
||||||
{...register('description')}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={styles.entryRow}>
|
|
||||||
<label className={styles.sectionTitle}>
|
|
||||||
Public info
|
|
||||||
<Textarea
|
|
||||||
variant='ontime-filled-on-light'
|
|
||||||
size='sm'
|
|
||||||
maxLength={150}
|
|
||||||
placeholder='Shows always start ontime'
|
|
||||||
{...register('publicInfo')}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={styles.entryRow}>
|
|
||||||
<label className={styles.sectionTitle}>
|
|
||||||
Public QR code Url
|
|
||||||
<Input
|
|
||||||
variant='ontime-filled-on-light'
|
|
||||||
size='sm'
|
|
||||||
placeholder='www.getontime.no'
|
|
||||||
{...register('publicUrl')}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={styles.entryRow}>
|
|
||||||
<label className={styles.sectionTitle}>
|
|
||||||
Backstage info
|
|
||||||
<Textarea
|
|
||||||
variant='ontime-filled-on-light'
|
|
||||||
size='sm'
|
|
||||||
maxLength={150}
|
|
||||||
placeholder='Wi-Fi password: 1234'
|
|
||||||
{...register('backstageInfo')}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={styles.entryRow}>
|
|
||||||
<label className={styles.sectionTitle}>
|
|
||||||
Backstage QR code Url
|
|
||||||
<Input
|
|
||||||
variant='ontime-filled-on-light'
|
|
||||||
size='sm'
|
|
||||||
placeholder='www.ontime.gitbook.io'
|
|
||||||
{...register('backstageUrl')}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<ModalFooter className={styles.buttonSection}>
|
|
||||||
<Button onClick={onReset} isDisabled={disableButtons} variant='ontime-ghost-on-light' size='sm'>
|
|
||||||
Clear data
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type='submit'
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={disableButtons}
|
|
||||||
variant='ontime-filled'
|
|
||||||
padding='0 2em'
|
|
||||||
size='sm'
|
|
||||||
>
|
|
||||||
New project file
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</form>
|
|
||||||
</ModalBody>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
||||||
import { httpIntegration } from '../services/integration-service/HttpIntegration.js';
|
import { httpIntegration } from '../services/integration-service/HttpIntegration.js';
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
import { deleteAllEvents, notifyChanges, setRundown } from '../services/rundown-service/RundownService.js';
|
import { notifyChanges, setRundown } from '../services/rundown-service/RundownService.js';
|
||||||
import { integrationService } from '../services/integration-service/IntegrationService.js';
|
import { integrationService } from '../services/integration-service/IntegrationService.js';
|
||||||
import { getProjectFiles } from '../utils/getFileListFromFolder.js';
|
import { getProjectFiles } from '../utils/getFileListFromFolder.js';
|
||||||
import { configService } from '../services/ConfigService.js';
|
import { configService } from '../services/ConfigService.js';
|
||||||
@@ -41,6 +41,8 @@ import { validateProjectFiles } from './ontimeController.validate.js';
|
|||||||
import { dbModel } from '../models/dataModel.js';
|
import { dbModel } from '../models/dataModel.js';
|
||||||
import { sheet } from '../utils/sheetsAuth.js';
|
import { sheet } from '../utils/sheetsAuth.js';
|
||||||
import { removeFileExtension } from '../utils/removeFileExtension.js';
|
import { removeFileExtension } from '../utils/removeFileExtension.js';
|
||||||
|
import { ensureJsonExtension } from '../utils/ensureJsonExtension.js';
|
||||||
|
import { generateUniqueFileName } from '../utils/generateUniqueFilename.js';
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/poll'
|
// Create controller for GET request to '/ontime/poll'
|
||||||
// Returns data for current state
|
// Returns data for current state
|
||||||
@@ -460,29 +462,6 @@ export async function previewExcel(req, res: Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Meant to create a new project file, it will clear only fields which are specific to a project
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const postNew: RequestHandler = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const newProjectData: ProjectData = {
|
|
||||||
title: req.body?.title ?? '',
|
|
||||||
description: req.body?.description ?? '',
|
|
||||||
publicUrl: req.body?.publicUrl ?? '',
|
|
||||||
publicInfo: req.body?.publicInfo ?? '',
|
|
||||||
backstageUrl: req.body?.backstageUrl ?? '',
|
|
||||||
backstageInfo: req.body?.backstageInfo ?? '',
|
|
||||||
};
|
|
||||||
const newData = await DataProvider.setProjectData(newProjectData);
|
|
||||||
await deleteAllEvents();
|
|
||||||
res.status(201).send(newData);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves and lists all project files from the uploads directory.
|
* Retrieves and lists all project files from the uploads directory.
|
||||||
* @param req
|
* @param req
|
||||||
@@ -618,20 +597,39 @@ export const renameProjectFile: RequestHandler = async (req: Request, res: Respo
|
|||||||
*/
|
*/
|
||||||
export const createProjectFile: RequestHandler = async (req: Request, res: Response) => {
|
export const createProjectFile: RequestHandler = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { filename } = req.body;
|
const originalFilename = ensureJsonExtension(req.body.title || 'Untitled');
|
||||||
|
const filename = generateUniqueFileName(uploadsFolderPath, originalFilename);
|
||||||
|
|
||||||
const projectFilePath = join(uploadsFolderPath, filename);
|
const projectFilePath = join(uploadsFolderPath, filename);
|
||||||
|
|
||||||
const errors = validateProjectFiles({ newFilename: filename });
|
const errors = validateProjectFiles({ newFilename: filename });
|
||||||
|
|
||||||
|
const newProjectData: ProjectData = {
|
||||||
|
title: req.body?.title ?? '',
|
||||||
|
description: req.body?.description ?? '',
|
||||||
|
publicUrl: req.body?.publicUrl ?? '',
|
||||||
|
publicInfo: req.body?.publicInfo ?? '',
|
||||||
|
backstageUrl: req.body?.backstageUrl ?? '',
|
||||||
|
backstageInfo: req.body?.backstageInfo ?? '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
...dbModel,
|
||||||
|
project: {
|
||||||
|
...dbModel.project,
|
||||||
|
...newProjectData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
return res.status(409).send({ message: errors.join(', ') });
|
return res.status(409).send({ message: 'Project with title already exists' });
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeFile(projectFilePath, JSON.stringify(dbModel));
|
await writeFile(projectFilePath, JSON.stringify(data));
|
||||||
|
await parseAndApply(projectFilePath, req, res, {});
|
||||||
|
|
||||||
res.status(200).send({
|
res.status(200).send({
|
||||||
message: `Created project ${filename}`,
|
filename,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).send({ message: String(error) });
|
res.status(500).send({ message: String(error) });
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
patchPartialProjectFile,
|
patchPartialProjectFile,
|
||||||
poll,
|
poll,
|
||||||
postAliases,
|
postAliases,
|
||||||
postNew,
|
|
||||||
postOSC,
|
postOSC,
|
||||||
postOscSubscriptions,
|
postOscSubscriptions,
|
||||||
postSettings,
|
postSettings,
|
||||||
@@ -48,7 +47,6 @@ import {
|
|||||||
validateProjectDuplicate,
|
validateProjectDuplicate,
|
||||||
validateLoadProjectFile,
|
validateLoadProjectFile,
|
||||||
validateProjectRename,
|
validateProjectRename,
|
||||||
validateProjectCreate,
|
|
||||||
validateSheetid,
|
validateSheetid,
|
||||||
validateWorksheet,
|
validateWorksheet,
|
||||||
validateSheetOptions,
|
validateSheetOptions,
|
||||||
@@ -115,9 +113,6 @@ router.get('/http', getHTTP);
|
|||||||
// create route between controller and '/ontime/http' endpoint
|
// create route between controller and '/ontime/http' endpoint
|
||||||
router.post('/http', validateHTTP, postHTTP);
|
router.post('/http', validateHTTP, postHTTP);
|
||||||
|
|
||||||
// create route between controller and '/ontime/new' endpoint
|
|
||||||
router.post('/new', projectSanitiser, postNew);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/projects' endpoint
|
// create route between controller and '/ontime/projects' endpoint
|
||||||
router.get('/projects', listProjects);
|
router.get('/projects', listProjects);
|
||||||
|
|
||||||
@@ -131,7 +126,7 @@ router.post('/project/:filename/duplicate', validateProjectDuplicate, sanitizePr
|
|||||||
router.put('/project/:filename/rename', validateProjectRename, sanitizeProjectFilename, renameProjectFile);
|
router.put('/project/:filename/rename', validateProjectRename, sanitizeProjectFilename, renameProjectFile);
|
||||||
|
|
||||||
// create route between controller and '/ontime/project' endpoint
|
// create route between controller and '/ontime/project' endpoint
|
||||||
router.post('/project', validateProjectCreate, sanitizeProjectFilename, createProjectFile);
|
router.post('/project', projectSanitiser, createProjectFile);
|
||||||
|
|
||||||
// create route between controller and '/ontime/project/:filename' endpoint
|
// create route between controller and '/ontime/project/:filename' endpoint
|
||||||
router.delete('/project/:filename', sanitizeProjectFilename, deleteProjectFile);
|
router.delete('/project/:filename', sanitizeProjectFilename, deleteProjectFile);
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { existsSync } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique file name within the specified directory.
|
||||||
|
* If a file with the same name already exists, appends a counter to the filename.
|
||||||
|
*
|
||||||
|
* @param {string} directory - The directory to check for file existence.
|
||||||
|
* @param {string} filename - The original filename.
|
||||||
|
* @return {Promise<string>} A unique filename.
|
||||||
|
*/
|
||||||
|
export const generateUniqueFileName = (directory: string, filename: string) => {
|
||||||
|
const baseName = path.basename(filename, path.extname(filename));
|
||||||
|
const extension = path.extname(filename);
|
||||||
|
|
||||||
|
let counter = 0;
|
||||||
|
let uniqueFilename = filename;
|
||||||
|
|
||||||
|
while (existsSync(path.join(directory, uniqueFilename))) {
|
||||||
|
counter++;
|
||||||
|
// Append counter to filename if the file exists.
|
||||||
|
uniqueFilename = `${baseName} (${counter})${extension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueFilename;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user