mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-15 04:13:47 +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
|
||||
*/
|
||||
@@ -382,11 +375,15 @@ export async function deleteProject(filename: string): Promise<MessageResponse>
|
||||
/**
|
||||
* @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 decodedUrl = decodeURIComponent(url);
|
||||
const res = await axios.post(decodedUrl, {
|
||||
filename,
|
||||
});
|
||||
const res = await axios.post(decodedUrl, project);
|
||||
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 {
|
||||
action: 'duplicate' | 'rename' | 'create';
|
||||
action: 'duplicate' | 'rename';
|
||||
filename: string;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: ProjectFormValues) => Promise<void>;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createProject } from '../../../../common/api/ontimeApi';
|
||||
import { useProjectList } from '../../../../common/hooks-query/useProjectList';
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
||||
import ProjectCreateForm, { ProjectCreateFormValues } from './ProjectCreateForm';
|
||||
import ProjectListItem, { EditMode } from './ProjectListItem';
|
||||
|
||||
import style from './ProjectPanel.module.scss';
|
||||
@@ -28,15 +28,15 @@ export default function ProjectList({ isCreatingProject, onToggleCreate }: Proje
|
||||
setSubmitError(null);
|
||||
};
|
||||
|
||||
const handleSubmitCreate = async (values: ProjectFormValues) => {
|
||||
const handleSubmitCreate = async (values: ProjectCreateFormValues) => {
|
||||
try {
|
||||
setSubmitError(null);
|
||||
const filename = values.filename.trim();
|
||||
if (!filename) {
|
||||
setSubmitError('Project name cannot be empty');
|
||||
return;
|
||||
}
|
||||
await createProject(filename);
|
||||
const filename = values.title?.trim();
|
||||
|
||||
await createProject({
|
||||
...values,
|
||||
filename,
|
||||
});
|
||||
await refetch();
|
||||
handleToggleCreateMode();
|
||||
} catch (error) {
|
||||
@@ -82,13 +82,7 @@ export default function ProjectList({ isCreatingProject, onToggleCreate }: Proje
|
||||
{isCreatingProject ? (
|
||||
<tr className={style.createContainer}>
|
||||
<td colSpan={99}>
|
||||
<ProjectForm
|
||||
action='create'
|
||||
filename=''
|
||||
onSubmit={handleSubmitCreate}
|
||||
onCancel={handleToggleCreateMode}
|
||||
submitError=''
|
||||
/>
|
||||
<ProjectCreateForm onSubmit={handleSubmitCreate} onCancel={handleToggleCreateMode} submitError='' />
|
||||
{submitError && <span className={style.createSubmitError}>{submitError}</span>}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.createFormInputField {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -26,6 +30,14 @@
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.createActionButtons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $red-500;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary'
|
||||
import AppSettings from '../app-settings/AppSettings';
|
||||
import { SettingsOptionId, useSettingsStore } from '../app-settings/settingsStore';
|
||||
import MenuBar from '../menu/MenuBar';
|
||||
import QuickStart from '../modals/quick-start/QuickStart';
|
||||
import SheetsModal from '../modals/sheets-modal/SheetsModal';
|
||||
import UploadModal from '../modals/upload-modal/UploadModal';
|
||||
import Overview from '../overview/Overview';
|
||||
@@ -33,7 +32,6 @@ export default function Editor() {
|
||||
onOpen: onIntegrationModalOpen,
|
||||
onClose: onIntegrationModalClose,
|
||||
} = useDisclosure();
|
||||
const { isOpen: isQuickStartOpen, onOpen: onQuickStartOpen, onClose: onQuickStartClose } = useDisclosure();
|
||||
const { isOpen: isSheetsOpen, onOpen: onSheetsOpen, onClose: onSheetsClose } = useDisclosure();
|
||||
|
||||
// Set window title
|
||||
@@ -46,7 +44,6 @@ export default function Editor() {
|
||||
return (
|
||||
<>
|
||||
<ErrorBoundary>
|
||||
<QuickStart onClose={onQuickStartClose} isOpen={isQuickStartOpen} />
|
||||
<UploadModal onClose={onUploadModalClose} isOpen={isUploadModalOpen} />
|
||||
<IntegrationModal onClose={onIntegrationModalClose} isOpen={isIntegrationModalOpen} />
|
||||
<SettingsModal isOpen={isOldSettingsOpen} onClose={onSettingsClose} />
|
||||
@@ -62,8 +59,6 @@ export default function Editor() {
|
||||
onUploadOpen={onUploadModalOpen}
|
||||
isIntegrationOpen={isIntegrationModalOpen}
|
||||
onIntegrationOpen={onIntegrationModalOpen}
|
||||
isQuickStartOpen={isQuickStartOpen}
|
||||
onQuickStartOpen={onQuickStartOpen}
|
||||
openSettings={handleSettings}
|
||||
isSettingsOpen={isSettingsOpen}
|
||||
isSheetsOpen={isSheetsOpen}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { IconButton, MenuButton, Tooltip } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
import { IoCloud } from '@react-icons/all-files/io5/IoCloud';
|
||||
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 { IoExtensionPuzzleOutline } from '@react-icons/all-files/io5/IoExtensionPuzzleOutline';
|
||||
import { IoOptions } from '@react-icons/all-files/io5/IoOptions';
|
||||
@@ -30,8 +29,6 @@ interface MenuBarProps {
|
||||
onUploadOpen: () => void;
|
||||
isIntegrationOpen: boolean;
|
||||
onIntegrationOpen: () => void;
|
||||
isQuickStartOpen: boolean;
|
||||
onQuickStartOpen: () => void;
|
||||
isSheetsOpen: boolean;
|
||||
onSheetsOpen: () => void;
|
||||
openSettings: (newTab?: string) => void;
|
||||
@@ -59,8 +56,6 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
onUploadOpen,
|
||||
isIntegrationOpen,
|
||||
onIntegrationOpen,
|
||||
isQuickStartOpen,
|
||||
onQuickStartOpen,
|
||||
openSettings,
|
||||
isSettingsOpen,
|
||||
isSheetsOpen,
|
||||
@@ -112,15 +107,6 @@ const MenuBar = (props: MenuBarProps) => {
|
||||
<div className={style.menu}>
|
||||
<QuitIconBtn disabled={!isElectron} clickHandler={sendShutdown} />
|
||||
<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
|
||||
{...buttonStyle}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user