refactor: restructure settings

refactor: migrate react components
This commit is contained in:
Carlos Valente
2025-07-04 12:13:06 +02:00
parent a8ea1080f3
commit 5600235ba1
93 changed files with 711 additions and 906 deletions
@@ -1,6 +1,7 @@
.subtle {
background: $gray-1050;
color: $blue-400;
line-height: 1em;
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
@@ -116,4 +117,24 @@
&:disabled {
opacity: $opacity-disabled;
}
}
}
.ghosted-destructive {
background: transparent;
color: $red-500;
&:hover:not(:disabled):not(:active) {
background: $gray-1000;
color: $red-500;
}
&:active:not(:disabled) {
background: $gray-1100;
border-color: $gray-1250;
}
&:disabled {
opacity: $opacity-disabled;
}
}
@@ -6,7 +6,15 @@ import { cx } from '../../utils/styleUtils';
import style from './Button.module.scss';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'subtle' | 'subtle-white' | 'destructive' | 'subtle-destructive' | 'ghosted' | 'ghosted-white';
variant?:
| 'primary'
| 'subtle'
| 'subtle-white'
| 'destructive'
| 'subtle-destructive'
| 'ghosted'
| 'ghosted-white'
| 'ghosted-destructive';
size?: 'small' | 'medium' | 'large' | 'xlarge';
fluid?: boolean;
loading?: boolean;
@@ -5,7 +5,15 @@ import { cx } from '../../utils/styleUtils';
import style from './IconButton.module.scss';
interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'subtle' | 'subtle-white' | 'destructive' | 'subtle-destructive' | 'ghosted' | 'ghosted-white';
variant?:
| 'primary'
| 'subtle'
| 'subtle-white'
| 'destructive'
| 'subtle-destructive'
| 'ghosted'
| 'ghosted-white'
| 'ghosted-destructive';
size?: 'small' | 'medium' | 'large' | 'xlarge';
}
@@ -7,15 +7,29 @@
border-radius: 3px;
font-size: $text-body-size;
padding: 1rem;
.content {
color: $gray-200;
}
color: $gray-200;
svg {
min-width: 1.5rem;
align-self: start;
font-size: 1.5rem;
}
}
.info {
svg {
color: $info-blue;
}
}
.warning {
svg {
color: $orange-500;
}
}
.error {
svg {
color: $red-500;
}
}
@@ -1,5 +1,5 @@
import { PropsWithChildren } from 'react';
import { IoAlertCircle } from 'react-icons/io5';
import { IoAlertCircle, IoWarning } from 'react-icons/io5';
import { cx } from '../../utils/styleUtils';
@@ -7,14 +7,15 @@ import style from './Info.module.scss';
interface InfoProps {
className?: string;
type?: 'info' | 'warning' | 'error';
}
export default function Info(props: PropsWithChildren<InfoProps>) {
const { className, children } = props;
export default function Info({ className, type = 'info', children }: PropsWithChildren<InfoProps>) {
return (
<div className={cx([style.infoLabel, className])}>
<IoAlertCircle />
<div className={cx([style.infoLabel, style[type], className])}>
{type === 'info' && <IoAlertCircle />}
{type === 'warning' && <IoWarning />}
{type === 'error' && <IoWarning />}
<div>{children}</div>
</div>
);
@@ -1,6 +1,5 @@
import { memo } from 'react';
import { useDisclosure } from '@chakra-ui/react';
import { useHotkeys } from '@mantine/hooks';
import { useDisclosure, useHotkeys } from '@mantine/hooks';
import FloatingNavigation from './floating-navigation/FloatingNavigation';
import ViewLockedIcon from './view-locked-icon/ViewLockedIcon';
@@ -14,17 +13,15 @@ interface ViewNavigationMenuProps {
export default memo(ViewNavigationMenu);
function ViewNavigationMenu({ isLockable, supressSettings }: ViewNavigationMenuProps) {
const { isOpen: isMenuOpen, onOpen: onMenuOpen, onClose: onMenuClose } = useDisclosure();
const [isMenuOpen, menuHandler] = useDisclosure();
const { showEditFormDrawer, isViewLocked } = useViewEditor({ isLockable });
const toggleMenu = () => (isMenuOpen ? onMenuClose() : onMenuOpen());
useHotkeys([
[
'Space',
() => {
if (isViewLocked) return;
toggleMenu();
menuHandler.toggle();
},
{ preventDefault: true },
],
@@ -45,10 +42,10 @@ function ViewNavigationMenu({ isLockable, supressSettings }: ViewNavigationMenuP
return (
<>
<FloatingNavigation
toggleMenu={toggleMenu}
toggleMenu={menuHandler.toggle}
toggleSettings={supressSettings ? undefined : () => showEditFormDrawer()}
/>
<NavigationMenu isOpen={isMenuOpen} onClose={onMenuClose} />
<NavigationMenu isOpen={isMenuOpen} onClose={menuHandler.close} />
</>
);
}
@@ -1,40 +0,0 @@
import { IoPause, IoPlay, IoStop } from 'react-icons/io5';
import { Tooltip } from '@chakra-ui/react';
import { Playback } from 'ontime-types';
import { tooltipDelayFast } from '../../../ontimeConfig';
interface PlaybackIconProps {
state: Playback;
skipTooltip?: boolean;
className?: string;
}
export default function PlaybackIcon(props: PlaybackIconProps) {
const { state, skipTooltip, className } = props;
// if timer is Pause or Armed
let label = 'Timer Paused';
let Icon = IoPause;
if (state === Playback.Roll) {
label = 'Timer Rolling';
Icon = IoPlay;
} else if (state === Playback.Play) {
label = 'Timer Playing';
Icon = IoPlay;
} else if (state === Playback.Stop) {
label = 'Timer Stopped';
Icon = IoStop;
}
if (skipTooltip) {
return <Icon className={className} />;
}
return (
<Tooltip openDelay={tooltipDelayFast} label={label} shouldWrapChildren>
<Icon className={className} />
</Tooltip>
);
}
@@ -4,12 +4,12 @@ import { useKeyDown } from '../../common/hooks/useKeyDown';
import AboutPanel from './panel/about-panel/AboutPanel';
import AutomationPanel from './panel/automations-panel/AutomationPanel';
import FeatureSettingsPanel from './panel/feature-settings-panel/FeatureSettingsPanel';
import GeneralPanel from './panel/general-panel/GeneralPanel';
import FeaturePanel from './panel/feature-panel/FeaturePanel';
import ManagePanel from './panel/manage-panel/ManagePanel';
import NetworkLogPanel from './panel/network-panel/NetworkLogPanel';
import ProjectPanel from './panel/project-panel/ProjectPanel';
import SettingsPanel from './panel/settings-panel/SettingsPanel';
import ShutdownPanel from './panel/shutdown-panel/ShutdownPanel';
import SourcesPanel from './panel/sources-panel/SourcesPanel';
import PanelContent from './panel-content/PanelContent';
import PanelList from './panel-list/PanelList';
import useAppSettingsNavigation from './useAppSettingsNavigation';
@@ -25,11 +25,11 @@ export default function AppSettings() {
<ErrorBoundary>
<PanelList selectedPanel={panel} location={location} />
<PanelContent onClose={close}>
{panel === 'settings' && <SettingsPanel location={location} />}
{panel === 'project' && <ProjectPanel location={location} setLocation={setLocation} />}
{panel === 'general' && <GeneralPanel location={location} />}
{panel === 'feature_settings' && <FeatureSettingsPanel location={location} />}
{panel === 'sources' && <SourcesPanel />}
{panel === 'manage' && <ManagePanel location={location} />}
{panel === 'automation' && <AutomationPanel location={location} />}
{panel === 'sharing' && <FeaturePanel location={location} />}
{panel === 'network' && <NetworkLogPanel location={location} />}
{panel === 'about' && <AboutPanel />}
{panel === 'shutdown' && <ShutdownPanel />}
@@ -68,12 +68,12 @@ function PanelListItem(props: PanelListItemProps) {
>
{panel.label}
</li>
{panel.secondary?.map((secondary) => {
{panel.secondary?.map((secondary, index) => {
const id = secondary.id.split('__')[1];
const secondaryClasses = cx([style.secondary, isSelected && location === id ? style.active : null]);
return (
<li
key={secondary.id}
key={secondary.id + index}
onClick={() => setLocation(secondary.id as SettingsOptionId)}
onKeyDown={(event) => {
isKeyEnter(event) && setLocation(secondary.id as SettingsOptionId);
@@ -188,6 +188,7 @@ $inner-padding: 1rem;
button {
margin-top: 1rem;
margin-inline: auto;
}
}
@@ -1,7 +1,7 @@
import { HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
import { IoAdd } from 'react-icons/io5';
import { Button } from '@chakra-ui/react';
import Button from '../../../common/components/buttons/Button';
import { cx } from '../../../common/utils/styleUtils';
import style from './PanelUtils.module.scss';
@@ -68,14 +68,8 @@ export function TableEmpty({ label, handleClick }: { label?: string; handleClick
<td colSpan={99}>
<div>{label ?? 'No data yet'}</div>
{handleClick && (
<Button
onClick={handleClick}
isDisabled={!handleClick}
variant='ontime-filled'
rightIcon={<IoAdd />}
size='sm'
>
New
<Button onClick={handleClick} disabled={!handleClick} variant='primary'>
New <IoAdd />
</Button>
)}
</td>
@@ -1,7 +1,7 @@
import { useEffect, useMemo } from 'react';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { IoAdd, IoTrash } from 'react-icons/io5';
import { Button, IconButton, Input, Radio, RadioGroup, Select } from '@chakra-ui/react';
import { Radio, RadioGroup, Select } from '@chakra-ui/react';
import {
Automation,
AutomationDTO,
@@ -15,7 +15,10 @@ import {
import { addAutomation, editAutomation, testOutput } from '../../../../common/api/automation';
import { maybeAxiosError } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import IconButton from '../../../../common/components/buttons/IconButton';
import Info from '../../../../common/components/info/Info';
import Input from '../../../../common/components/input/input/Input';
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
import Tag from '../../../../common/components/tag/Tag';
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
@@ -198,10 +201,8 @@ export default function AutomationForm(props: AutomationFormProps) {
Title
<Input
{...register('title', { required: { value: true, message: 'Required field' } })}
variant='ontime-filled'
size='sm'
fluid
placeholder='Load preset'
autoComplete='off'
/>
</label>
<Panel.Error>{errors.title?.message}</Panel.Error>
@@ -272,43 +273,22 @@ export default function AutomationForm(props: AutomationFormProps) {
</label>
<label>
Value to match
<Input
{...register(`filters.${index}.value`)}
variant='ontime-filled'
size='sm'
placeholder='<empty / no value>'
autoComplete='off'
/>
<Input {...register(`filters.${index}.value`)} fluid placeholder='<empty / no value>' />
</label>
<div>
<span>&nbsp;</span>
<div>
<IconButton
aria-label='Delete'
icon={<IoTrash />}
variant='ontime-ghosted'
size='sm'
color='#FA5656' // $red-500
onClick={() => removeFilter(index)}
isDisabled={false}
isLoading={false}
/>
<IconButton aria-label='Delete' variant='ghosted-destructive' onClick={() => removeFilter(index)}>
<IoTrash />
</IconButton>
</div>
</div>
</div>
);
})}
<div>
<Button
variant='ontime-subtle'
size='sm'
type='submit'
rightIcon={<IoAdd />}
onClick={handleAddNewFilter}
isDisabled={false}
isLoading={false}
>
Add filter
<Button type='submit' onClick={handleAddNewFilter}>
Add filter <IoAdd />
</Button>
</div>
</div>
@@ -342,10 +322,8 @@ export default function AutomationForm(props: AutomationFormProps) {
{...register(`outputs.${index}.targetIP`, {
required: { value: true, message: 'Required field' },
})}
variant='ontime-filled'
size='sm'
fluid
placeholder='127.0.0.1'
autoComplete='off'
/>
<Panel.Error>{rowErrors?.targetIP?.message}</Panel.Error>
</label>
@@ -358,51 +336,32 @@ export default function AutomationForm(props: AutomationFormProps) {
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
min: { value: 1024, message: 'Port must be within range 1024 - 65535' },
})}
variant='ontime-filled'
size='sm'
fluid
type='number'
maxLength={5}
placeholder='8000'
autoComplete='off'
/>
<Panel.Error>{rowErrors?.targetPort?.message}</Panel.Error>
</label>
<label>
Address
<Input
{...register(`outputs.${index}.address`)}
variant='ontime-filled'
size='sm'
placeholder='/cue/start'
autoComplete='off'
/>
<Input {...register(`outputs.${index}.address`)} fluid placeholder='/cue/start' />
<Panel.Error>{rowErrors?.address?.message}</Panel.Error>
</label>
<label>
Arguments
<TemplateInput
{...register(`outputs.${index}.args`)}
value={output.args}
variant='ontime-filled'
size='sm'
placeholder='1'
/>
<TemplateInput {...register(`outputs.${index}.args`)} value={output.args} placeholder='1' />
<Panel.Error>{rowErrors?.args?.message}</Panel.Error>
</label>
<div>
<span>&nbsp;</span>
<Panel.InlineElements relation='inner'>
<Button size='sm' variant='ontime-ghosted-white' onClick={() => handleTestOSCOutput(index)}>
<Button variant='ghosted-white' onClick={() => handleTestOSCOutput(index)}>
Test
</Button>
<IconButton
aria-label='Delete'
icon={<IoTrash />}
variant='ontime-ghosted'
size='sm'
onClick={() => removeOutput(index)}
color='#FA5656' // $red-500
/>
<IconButton aria-label='Delete' variant='ghosted-destructive' onClick={() => removeOutput(index)}>
<IoTrash />
</IconButton>
</Panel.InlineElements>
</div>
</div>
@@ -429,27 +388,20 @@ export default function AutomationForm(props: AutomationFormProps) {
message: 'HTTP messages should target http:// or https://',
},
})}
variant='ontime-filled'
size='sm'
fluid
placeholder='http://127.0.0.1/start/1'
autoComplete='off'
/>
<Panel.Error>{rowErrors?.url?.message}</Panel.Error>
</label>
<div>
<span>&nbsp;</span>
<Panel.InlineElements relation='inner'>
<Button size='sm' variant='ontime-ghosted-white' onClick={() => handleTestHTTPOutput(index)}>
<Button variant='ghosted-white' onClick={() => handleTestHTTPOutput(index)}>
Test
</Button>
<IconButton
aria-label='Delete'
icon={<IoTrash />}
variant='ontime-ghosted'
size='sm'
onClick={() => removeOutput(index)}
color='#FA5656' // $red-500
/>
<IconButton aria-label='Delete' variant='ghosted-destructive' onClick={() => removeOutput(index)}>
<IoTrash />
</IconButton>
</Panel.InlineElements>
</div>
</div>
@@ -479,17 +431,12 @@ export default function AutomationForm(props: AutomationFormProps) {
>
<span>&nbsp;</span>
<Panel.InlineElements relation='inner'>
<Button size='sm' variant='ontime-ghosted-white' onClick={() => handleTestOntimeAction(index)}>
<Button variant='ghosted-white' onClick={() => handleTestOntimeAction(index)}>
Test
</Button>
<IconButton
aria-label='Delete'
icon={<IoTrash />}
variant='ontime-ghosted'
size='sm'
onClick={() => removeOutput(index)}
color='#FA5656' // $red-500
/>
<IconButton aria-label='Delete' variant='ghosted-destructive' onClick={() => removeOutput(index)}>
<IoTrash />
</IconButton>
</Panel.InlineElements>
</OntimeActionForm>
</div>
@@ -500,24 +447,22 @@ export default function AutomationForm(props: AutomationFormProps) {
return null;
})}
<Panel.InlineElements relation='inner'>
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleAddNewOSCOutput}>
OSC
<Button onClick={handleAddNewOSCOutput}>
OSC <IoAdd />
</Button>
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleAddNewHTTPOutput}>
HTTP
<Button onClick={handleAddNewHTTPOutput}>
HTTP <IoAdd />
</Button>
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleAddnewOntimeAction}>
Ontime action
<Button onClick={handleAddnewOntimeAction}>
Ontime action <IoAdd />
</Button>
</Panel.InlineElements>
</div>
<Panel.InlineElements align='end'>
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
<Button variant='ontime-subtle' size='sm' onClick={onClose}>
Cancel
</Button>
<Button variant='ontime-filled' size='sm' type='submit' isDisabled={!canSubmit} isLoading={isSubmitting}>
<Button onClick={onClose}>Cancel</Button>
<Button variant='primary' type='submit' disabled={!canSubmit} loading={isSubmitting}>
Save
</Button>
</Panel.InlineElements>
@@ -1,9 +1,11 @@
import { Controller, useForm } from 'react-hook-form';
import { Button, Input, Switch } from '@chakra-ui/react';
import { Switch } from '@chakra-ui/react';
import { editAutomationSettings } from '../../../../common/api/automation';
import { maybeAxiosError } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import Info from '../../../../common/components/info/Info';
import Input from '../../../../common/components/input/input/Input';
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
import { preventEscape } from '../../../../common/utils/keyEvent';
import { isOnlyNumbers } from '../../../../common/utils/regex';
@@ -57,16 +59,15 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
<Panel.SubHeader>
Automation settings
<Panel.InlineElements>
<Button variant='ontime-ghosted' size='sm' onClick={onReset} isDisabled={!canSubmit}>
<Button variant='ghosted' onClick={onReset} disabled={!canSubmit}>
Revert to saved
</Button>
<Button
variant='ontime-filled'
size='sm'
variant='primary'
type='submit'
form='automation-settings-form'
isDisabled={!canSubmit}
isLoading={isSubmitting}
disabled={!canSubmit}
loading={isSubmitting}
>
Save
</Button>
@@ -139,13 +140,10 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
<Input
id='oscPortIn'
placeholder='8888'
width='5rem'
maxLength={5}
size='sm'
textAlign='right'
variant='ontime-filled'
style={{ textAlign: 'right', width: '5rem' }}
type='number'
autoComplete='off'
fluid
{...register('oscPortIn', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
@@ -1,10 +1,11 @@
import { Fragment, useState } from 'react';
import { IoAdd, IoPencil, IoTrash } from 'react-icons/io5';
import { Button, IconButton } from '@chakra-ui/react';
import { AutomationDTO, NormalisedAutomation } from 'ontime-types';
import { deleteAutomation } from '../../../../common/api/automation';
import { maybeAxiosError } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import IconButton from '../../../../common/components/buttons/IconButton';
import Tag from '../../../../common/components/tag/Tag';
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
import * as Panel from '../../panel-utils/PanelUtils';
@@ -46,14 +47,11 @@ export default function AutomationsList(props: AutomationsListProps) {
<Panel.SubHeader>
Manage automations
<Button
variant='ontime-subtle'
rightIcon={<IoAdd />}
size='sm'
type='submit'
isDisabled={Boolean(automationFormData)}
disabled={Boolean(automationFormData)}
onClick={() => setAutomationFormData(automationPlaceholder)}
>
New
New <IoAdd />
</Button>
</Panel.SubHeader>
@@ -94,21 +92,19 @@ export default function AutomationsList(props: AutomationsListProps) {
<td>{automations[automationId].outputs.length}</td>
<Panel.InlineElements align='end' relation='inner' as='td'>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#e2e2e2' // $gray-200
icon={<IoPencil />}
variant='ghosted-white'
aria-label='Edit entry'
onClick={() => setAutomationFormData(automations[automationId])}
/>
>
<IoPencil />
</IconButton>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#FA5656' // $red-500
icon={<IoTrash />}
variant='ghosted-destructive'
aria-label='Delete entry'
onClick={() => handleDelete(automationId)}
/>
>
<IoTrash />
</IconButton>
</Panel.InlineElements>
</tr>
{deleteError && (
@@ -1,8 +1,9 @@
import { PropsWithChildren, useState } from 'react';
import { UseFormRegister, UseFormSetValue } from 'react-hook-form';
import { Input, Select } from '@chakra-ui/react';
import { Select } from '@chakra-ui/react';
import { AutomationDTO, OntimeAction } from 'ontime-types';
import Input from '../../../../common/components/input/input/Input';
import { cx } from '../../../../common/utils/styleUtils';
import * as Panel from '../../panel-utils/PanelUtils';
@@ -67,10 +68,8 @@ export default function OntimeActionForm(props: PropsWithChildren<OntimeActionFo
{...register(`outputs.${index}.time`, {
required: { value: true, message: 'Required field' },
})}
variant='ontime-filled'
size='sm'
fluid
placeholder='eg: 10m5s'
autoComplete='off'
/>
<Panel.Error>{rowErrors?.time?.message}</Panel.Error>
</label>
@@ -80,13 +79,7 @@ export default function OntimeActionForm(props: PropsWithChildren<OntimeActionFo
<>
<label>
Text (leave empty for no change)
<Input
{...register(`outputs.${index}.text`)}
variant='ontime-filled'
size='sm'
placeholder='eg: Timer is finished'
autoComplete='off'
/>
<Input {...register(`outputs.${index}.text`)} fluid placeholder='eg: Timer is finished' />
<Panel.Error>{rowErrors?.text?.message}</Panel.Error>
</label>
<label>
@@ -1,10 +1,12 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input, Select } from '@chakra-ui/react';
import { Select } from '@chakra-ui/react';
import { NormalisedAutomation, TimerLifeCycle, TriggerDTO } from 'ontime-types';
import { addTrigger, editTrigger } from '../../../../common/api/automation';
import { maybeAxiosError } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import Input from '../../../../common/components/input/input/Input';
import { preventEscape } from '../../../../common/utils/keyEvent';
import * as Panel from '../../panel-utils/PanelUtils';
@@ -87,9 +89,7 @@ export default function TriggerForm(props: TriggerFormProps) {
Title
<Input
{...register('title', { required: { value: true, message: 'Required field' } })}
size='sm'
variant='ontime-filled'
autoComplete='off'
fluid
defaultValue={initialTitle}
/>
<Panel.Error>{errors.title?.message}</Panel.Error>
@@ -127,10 +127,10 @@ export default function TriggerForm(props: TriggerFormProps) {
<Panel.Error>{errors.automationId?.message}</Panel.Error>
</label>
<Panel.InlineElements align='end'>
<Button size='sm' variant='ontime-subtle' isDisabled={isSubmitting} onClick={onCancel}>
<Button disabled={isSubmitting} onClick={onCancel}>
Cancel
</Button>
<Button type='submit' size='sm' variant='ontime-filled' isDisabled={!canSubmit} isLoading={isSubmitting}>
<Button type='submit' variant='primary' disabled={!canSubmit} loading={isSubmitting}>
Save
</Button>
</Panel.InlineElements>
@@ -1,10 +1,10 @@
import { Fragment, useMemo, useState } from 'react';
import { IoAdd } from 'react-icons/io5';
import { Button } from '@chakra-ui/react';
import { NormalisedAutomation, Trigger } from 'ontime-types';
import { deleteTrigger } from '../../../../common/api/automation';
import { maybeAxiosError } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
import * as Panel from '../../panel-utils/PanelUtils';
@@ -47,17 +47,8 @@ export default function TriggersList(props: TriggersListProps) {
<Panel.Card>
<Panel.SubHeader>
Manage triggers
<Button
variant='ontime-subtle'
rightIcon={<IoAdd />}
size='sm'
type='submit'
form='trigger-form'
isDisabled={!canAdd}
isLoading={false}
onClick={() => setShowForm(true)}
>
New
<Button type='submit' form='trigger-form' disabled={!canAdd} loading={false} onClick={() => setShowForm(true)}>
New <IoAdd />
</Button>
</Panel.SubHeader>
<Panel.Divider />
@@ -1,8 +1,8 @@
import { useState } from 'react';
import { IoPencil, IoTrash, IoWarningOutline } from 'react-icons/io5';
import { IconButton } from '@chakra-ui/react';
import { NormalisedAutomation, TimerLifeCycle } from 'ontime-types';
import IconButton from '../../../../common/components/buttons/IconButton';
import Tag from '../../../../common/components/tag/Tag';
import * as Panel from '../../panel-utils/PanelUtils';
@@ -62,22 +62,12 @@ export default function TriggersListItem(props: TriggersListItemProps) {
<Tag>{automations?.[automationId]?.title}</Tag>
</td>
<Panel.InlineElements align='end' relation='inner' as='td'>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#e2e2e2' // $gray-200
icon={<IoPencil />}
aria-label='Edit entry'
onClick={() => setIsEditing(true)}
/>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#FA5656' // $red-500
icon={<IoTrash />}
aria-label='Delete entry'
onClick={handleDelete}
/>
<IconButton variant='ghosted-white' aria-label='Edit entry' onClick={() => setIsEditing(true)}>
<IoPencil />
</IconButton>
<IconButton variant='ghosted-destructive' aria-label='Delete entry' onClick={handleDelete}>
<IoTrash />
</IconButton>
</Panel.InlineElements>
</tr>
);
@@ -1,7 +1,7 @@
import { forwardRef, useMemo, useState } from 'react';
import { type InputProps, Input } from '@chakra-ui/react';
import { mergeRefs, useClickOutside } from '@mantine/hooks';
import Input, { type InputProps } from '../../../../../common/components/input/input/Input';
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
import { makeAutoCompleteList, matchRemaining, selectFromLastTemplate } from './templateInput.utils';
@@ -53,7 +53,7 @@ const TemplateInput = forwardRef(function TemplateInput(props: TemplateInputProp
return (
<div className={style.wrapper} ref={mergeRefs(localRef, ref)}>
<Input value={inputValue} {...rest} onChange={handleInputChange} autoComplete='off' autoCorrect='off' />
<Input value={inputValue} {...rest} onChange={handleInputChange} fluid />
{showSuggestions && suggestions.length > 0 && (
<ul className={style.suggestions}>
{suggestions.map((suggestion) => (
@@ -0,0 +1,11 @@
.fit {
width: fit-content;
}
.aliasConstrain {
min-width: 12em;
}
.fullWidth {
width: 100%;
}
@@ -0,0 +1,42 @@
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { isOntimeCloud } from '../../../../externals';
import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils';
import InfoNif from '../network-panel/NetworkInterfaces';
import GenerateLinkFormExport from './GenerateLinkFormExport';
import ReportSettings from './ReportSettings';
import UrlPresetsForm from './UrlPresetsForm';
export default function FeaturePanel({ location }: PanelBaseProps) {
const presetsRef = useScrollIntoView<HTMLDivElement>('presets', location);
const linkRef = useScrollIntoView<HTMLDivElement>('link', location);
const reportRef = useScrollIntoView<HTMLDivElement>('report', location);
return (
<>
<Panel.Header>Sharing and reporting</Panel.Header>
<div ref={presetsRef}>
<UrlPresetsForm />
</div>
<div ref={linkRef}>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Share Ontime Link</Panel.SubHeader>
<Panel.Divider />
{!isOntimeCloud && (
<>
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
<InfoNif />
</>
)}
<GenerateLinkFormExport />
</Panel.Card>
</Panel.Section>
</div>
<div ref={reportRef}>
<ReportSettings />
</div>
</>
);
}
@@ -1,9 +1,9 @@
import { useMemo } from 'react';
import { IoTrashBin } from 'react-icons/io5';
import { Button } from '@chakra-ui/react';
import { deleteAllReport } from '../../../../common/api/report';
import { createBlob, downloadBlob } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import useReport from '../../../../common/hooks-query/useReport';
import useRundown from '../../../../common/hooks-query/useRundown';
import { cx } from '../../../../common/utils/styleUtils';
@@ -41,23 +41,12 @@ export default function ReportSettings() {
<Panel.Title>
Manage report
<Panel.InlineElements>
<Button
variant='ontime-subtle'
leftIcon={<IoTrashBin />}
size='sm'
onClick={() => downloadCSV(combinedReport)}
isDisabled={combinedReport.length === 0}
>
<Button onClick={() => downloadCSV(combinedReport)} disabled={combinedReport.length === 0}>
<IoTrashBin />
Export CSV
</Button>
<Button
variant='ontime-subtle'
leftIcon={<IoTrashBin />}
size='sm'
color='#FA5656'
onClick={clearReport}
isDisabled={combinedReport.length === 0}
>
<Button variant='subtle-destructive' onClick={clearReport} disabled={combinedReport.length === 0}>
<IoTrashBin />
Clear All
</Button>
</Panel.InlineElements>
@@ -1,13 +1,16 @@
import { useEffect } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { IoAdd, IoOpenOutline, IoTrash } from 'react-icons/io5';
import { Button, IconButton, Input, Switch } from '@chakra-ui/react';
import { Switch } from '@chakra-ui/react';
import { URLPreset } from 'ontime-types';
import { postUrlPresets } from '../../../../common/api/urlPresets';
import { maybeAxiosError } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import IconButton from '../../../../common/components/buttons/IconButton';
import TooltipActionBtn from '../../../../common/components/buttons/TooltipActionBtn';
import Info from '../../../../common/components/info/Info';
import Input from '../../../../common/components/input/input/Input';
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
import { preventEscape } from '../../../../common/utils/keyEvent';
@@ -15,7 +18,7 @@ import { handleLinks } from '../../../../common/utils/linkUtils';
import { validateUrlPresetPath } from '../../../../common/utils/urlPresets';
import * as Panel from '../../panel-utils/PanelUtils';
import style from './FeatureSettings.module.scss';
import style from './FeaturePanel.module.scss';
const urlPresetsDocs = 'https://docs.getontime.no/features/url-presets/';
@@ -98,10 +101,10 @@ export default function UrlPresetsForm() {
<Panel.SubHeader>
URL presets
<Panel.InlineElements>
<Button variant='ontime-ghosted' size='sm' onClick={onReset} isDisabled={!canSubmit}>
<Button variant='ghosted' onClick={onReset} disabled={!canSubmit}>
Revert to saved
</Button>
<Button variant='ontime-filled' size='sm' type='submit' isDisabled={!canSubmit} isLoading={isSubmitting}>
<Button variant='primary' type='submit' disabled={!canSubmit} loading={isSubmitting}>
Save
</Button>
</Panel.InlineElements>
@@ -134,8 +137,8 @@ export default function UrlPresetsForm() {
<Panel.Loader isLoading={isLoading} />
<Panel.Title>
Manage presets
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={addNew}>
New
<Button onClick={addNew}>
New <IoAdd />
</Button>
</Panel.Title>
{errors?.root && <Panel.Error>{errors.root.message}</Panel.Error>}
@@ -173,11 +176,9 @@ export default function UrlPresetsForm() {
{...register(`data.${index}.alias`, {
required: { value: true, message: 'Required field' },
})}
size='sm'
variant='ontime-filled'
fluid
placeholder='URL Preset'
data-testid={`field__alias_${index}`}
autoComplete='off'
/>
<Panel.Error>{maybeAliasError}</Panel.Error>
</td>
@@ -186,11 +187,9 @@ export default function UrlPresetsForm() {
{...register(`data.${index}.pathAndParams`, {
required: { value: true, message: 'Required field' },
})}
size='sm'
variant='ontime-filled'
fluid
placeholder='URL (portion after ontime Port)'
data-testid={`field__url_${index}`}
autoComplete='off'
/>
<Panel.Error>{maybeUrlError}</Panel.Error>
</td>
@@ -207,14 +206,13 @@ export default function UrlPresetsForm() {
data-testid={`field__test_${index}`}
/>
<IconButton
size='sm'
onClick={() => remove(index)}
variant='ontime-ghosted'
color='#FA5656' // $red-500
icon={<IoTrash />}
variant='ghosted-destructive'
aria-label='Delete entry'
data-testid={`field__delete_${index}`}
/>
>
<IoTrash />
</IconButton>
</Panel.InlineElements>
</tr>
);
@@ -1,30 +0,0 @@
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils';
import CustomFields from './custom-fields/CustomFields';
import ReportSettings from './ReportSettings';
import UrlPresetsForm from './UrlPresetsForm';
export default function FeatureSettingsPanel({ location }: PanelBaseProps) {
const customFieldsRef = useScrollIntoView<HTMLDivElement>('custom', location);
const urlPresetsRef = useScrollIntoView<HTMLDivElement>('urlpresets', location);
const reportRef = useScrollIntoView<HTMLDivElement>('report', location);
return (
<>
<Panel.Header>Feature Settings</Panel.Header>
<div ref={customFieldsRef}>
<CustomFields />
</div>
<div ref={urlPresetsRef}>
<UrlPresetsForm />
</div>
<div ref={reportRef}>
<ReportSettings />
</div>
</>
);
}
@@ -1,28 +0,0 @@
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils';
import EditorSettingsForm from '../interface-panel/EditorSettingsForm';
import GeneralPanelForm from './GeneralPanelForm';
import ViewSettingsForm from './ViewSettingsForm';
export default function GeneralPanel({ location }: PanelBaseProps) {
const generalRef = useScrollIntoView<HTMLDivElement>('settings', location);
const editorRef = useScrollIntoView<HTMLDivElement>('editor', location);
const viewRef = useScrollIntoView<HTMLDivElement>('view', location);
return (
<>
<Panel.Header>App Settings</Panel.Header>
<div ref={generalRef}>
<GeneralPanelForm />
</div>
<div ref={editorRef}>
<EditorSettingsForm />
</div>
<div ref={viewRef}>
<ViewSettingsForm />
</div>
</>
);
}
@@ -1,19 +1,19 @@
import { useState } from 'react';
import { IoAdd } from 'react-icons/io5';
import { Button } from '@chakra-ui/react';
import { CustomField, CustomFieldKey } from 'ontime-types';
import { deleteCustomField, editCustomField, postCustomField } from '../../../../../common/api/customFields';
import Info from '../../../../../common/components/info/Info';
import ExternalLink from '../../../../../common/components/link/external-link/ExternalLink';
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
import { customFieldsDocsUrl } from '../../../../../externals';
import * as Panel from '../../../panel-utils/PanelUtils';
import { deleteCustomField, editCustomField, postCustomField } from '../../../../common/api/customFields';
import Button from '../../../../common/components/buttons/Button';
import Info from '../../../../common/components/info/Info';
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
import useCustomFields from '../../../../common/hooks-query/useCustomFields';
import { customFieldsDocsUrl } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
import CustomFieldEntry from './CustomFieldEntry';
import CustomFieldForm from './CustomFieldForm';
import CustomFieldEntry from './composite/CustomFieldEntry';
import CustomFieldForm from './composite/CustomFieldForm';
export default function CustomFields() {
export default function CustomFieldSettings() {
const { data, refetch } = useCustomFields();
const [isAdding, setIsAdding] = useState(false);
@@ -50,8 +50,8 @@ export default function CustomFields() {
<Panel.Card>
<Panel.SubHeader>
Custom fields
<Button variant='ontime-subtle' rightIcon={<IoAdd />} size='sm' onClick={handleInitiateCreate}>
New
<Button onClick={handleInitiateCreate}>
New <IoAdd />
</Button>
</Panel.SubHeader>
<Panel.Divider />
@@ -60,11 +60,7 @@ export default function CustomFields() {
Custom fields allow for additional information to be added to an event.
<br />
<br />
This data is not used by Ontime, but provides place for cueing or department specific information (eg.
light, sound, camera).
<br />
<br />
Custom fields can be used width the Integrations feature using the generated key.
This data can be used in the Automation feature by using the generated key.
<ExternalLink href={customFieldsDocsUrl}>See the docs</ExternalLink>
</Info>
</Panel.Section>
@@ -14,16 +14,12 @@
gap: 1rem;
}
.fit {
width: fit-content;
}
.aliasConstrain {
min-width: 12em;
}
.twoCols {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.current {
background-color: $blue-1100;
}
@@ -0,0 +1,33 @@
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils';
import SourcesPanel from './sources-panel/SourcesPanel';
import CustomFieldSettings from './CustomFields';
import ManageRundowns from './ManageRundowns';
import RundownDefaultSettings from './RundownDefaultSettings';
export default function ManagePanel({ location }: PanelBaseProps) {
const defaultsRef = useScrollIntoView<HTMLDivElement>('defaults', location);
const customRef = useScrollIntoView<HTMLDivElement>('custom', location);
const rundownsRef = useScrollIntoView<HTMLDivElement>('rundowns', location);
const sheetsRef = useScrollIntoView<HTMLDivElement>('sheets', location);
return (
<>
<Panel.Header>Project data</Panel.Header>
<div ref={defaultsRef}>
<RundownDefaultSettings />
</div>
<div ref={customRef}>
<CustomFieldSettings />
</div>
<div ref={rundownsRef}>
<ManageRundowns />
</div>
<div ref={sheetsRef}>
<SourcesPanel />
</div>
</>
);
}
@@ -7,7 +7,7 @@ import { useProjectRundowns } from '../../../../common/hooks-query/useProjectRun
import { cx } from '../../../../common/utils/styleUtils';
import * as Panel from '../../panel-utils/PanelUtils';
import style from './ProjectPanel.module.scss';
import style from './ManagePanel.module.scss';
export default function ManageRundowns() {
const { data } = useProjectRundowns();
@@ -6,7 +6,7 @@ import TimeInput from '../../../../common/components/input/time-input/TimeInput'
import { editorSettingsDefaults, useEditorSettings } from '../../../../common/stores/editorSettings';
import * as Panel from '../../panel-utils/PanelUtils';
export default function EditorSettingsForm() {
export default function RundownDefaultSettings() {
const {
defaultDuration,
linkPrevious,
@@ -31,10 +31,10 @@ export default function EditorSettingsForm() {
return (
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Editor settings</Panel.SubHeader>
<Panel.SubHeader>Rundown defaults</Panel.SubHeader>
<Panel.Divider />
<Panel.Section>
<Panel.Title>Rundown defaults for new events</Panel.Title>
<Panel.Title>Default settings for new events</Panel.Title>
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field
@@ -126,44 +126,6 @@ export default function EditorSettingsForm() {
</Panel.ListItem>
</Panel.ListGroup>
</Panel.Section>
<Panel.Section>
<Panel.Title>Run mode</Panel.Title>
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field
title='Show quick entry'
description='Whether the quick entry buttons show above / under selected event'
/>
<Switch variant='ontime' size='lg' defaultChecked={false} isDisabled />
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field
title='Follow playback'
description='Whether view automatically follows the event being played'
/>
<Switch variant='ontime' size='lg' defaultChecked isDisabled />
</Panel.ListItem>
</Panel.ListGroup>
</Panel.Section>
<Panel.Section>
<Panel.Title>Edit mode</Panel.Title>
<Panel.ListGroup>
<Panel.ListItem>
<Panel.Field
title='Show quick entry'
description='Whether the quick entry buttons show above / under selected event'
/>
<Switch variant='ontime' size='lg' defaultChecked isDisabled />
</Panel.ListItem>
<Panel.ListItem>
<Panel.Field
title='Follow playback'
description='Whether view automatically follows the event being played'
/>
<Switch variant='ontime' size='lg' defaultChecked={false} isDisabled />
</Panel.ListItem>
</Panel.ListGroup>
</Panel.Section>
</Panel.Card>
</Panel.Section>
);
@@ -1,8 +1,8 @@
import { useState } from 'react';
import { IoPencil, IoTrash } from 'react-icons/io5';
import { IconButton } from '@chakra-ui/react';
import { CustomField, CustomFieldKey } from 'ontime-types';
import IconButton from '../../../../../common/components/buttons/IconButton';
import CopyTag from '../../../../../common/components/copy-tag/CopyTag';
import Swatch from '../../../../../common/components/input/colour-input/Swatch';
import Tag from '../../../../../common/components/tag/Tag';
@@ -10,7 +10,7 @@ import * as Panel from '../../../panel-utils/PanelUtils';
import CustomFieldForm from './CustomFieldForm';
import style from '../FeatureSettings.module.scss';
import style from '../ManagePanel.module.scss';
interface CustomFieldEntryProps {
colour: string;
@@ -61,22 +61,12 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) {
</CopyTag>
</td>
<Panel.InlineElements relation='inner' as='td'>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#e2e2e2' // $gray-200
icon={<IoPencil />}
aria-label='Edit entry'
onClick={() => setIsEditing(true)}
/>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#FA5656' // $red-500
icon={<IoTrash />}
aria-label='Delete entry'
onClick={() => onDelete(fieldKey)}
/>
<IconButton variant='ghosted-white' aria-label='Edit entry' onClick={() => setIsEditing(true)}>
<IoPencil />
</IconButton>
<IconButton variant='ghosted-destructive' aria-label='Delete entry' onClick={() => onDelete(fieldKey)}>
<IoTrash />
</IconButton>
</Panel.InlineElements>
</tr>
);
@@ -1,17 +1,19 @@
import { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Button, Input, Radio, RadioGroup } from '@chakra-ui/react';
import { Radio, RadioGroup } from '@chakra-ui/react';
import { CustomField } from 'ontime-types';
import { customFieldLabelToKey, isAlphanumericWithSpace } from 'ontime-utils';
import { maybeAxiosError } from '../../../../../common/api/utils';
import Button from '../../../../../common/components/buttons/Button';
import Info from '../../../../../common/components/info/Info';
import SwatchSelect from '../../../../../common/components/input/colour-input/SwatchSelect';
import Input from '../../../../../common/components/input/input/Input';
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
import { preventEscape } from '../../../../../common/utils/keyEvent';
import * as Panel from '../../../panel-utils/PanelUtils';
import style from '../FeatureSettings.module.scss';
import style from '../ManagePanel.module.scss';
interface CustomFieldsFormProps {
onSubmit: (field: CustomField) => Promise<void>;
@@ -118,15 +120,13 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
return true;
},
})}
size='sm'
variant='ontime-filled'
autoComplete='off'
fluid
/>
</div>
<div>
<Panel.Description>Key (use in Integrations and API)</Panel.Description>
<Input {...register('key')} disabled size='sm' variant='ontime-filled' autoComplete='off' />
<Input {...register('key')} readOnly fluid />
</div>
</div>
<div>
@@ -135,10 +135,10 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
</div>
{errors.root && <Panel.Error>{errors.root.message}</Panel.Error>}
<Panel.InlineElements relation='inner' align='end'>
<Button size='sm' variant='ontime-ghosted' onClick={onCancel}>
<Button variant='ghosted' onClick={onCancel}>
Cancel
</Button>
<Button size='sm' type='submit' variant='ontime-filled' isDisabled={!canSubmit} isLoading={isSubmitting}>
<Button type='submit' variant='primary' disabled={!canSubmit} loading={isSubmitting}>
Save
</Button>
</Panel.InlineElements>
@@ -1,5 +1,5 @@
import Info from '../../../../common/components/info/Info';
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
import Info from '../../../../../common/components/info/Info';
import ExternalLink from '../../../../../common/components/link/external-link/ExternalLink';
const googleSheetDocsUrl = 'https://docs.getontime.no/features/import-spreadsheet-gsheet/';
@@ -1,12 +1,13 @@
import { ChangeEvent, useEffect, useState } from 'react';
import { IoCheckmark, IoShieldCheckmarkOutline } from 'react-icons/io5';
import { Button, Input, Spinner } from '@chakra-ui/react';
import { getWorksheetNames } from '../../../../common/api/sheets';
import { maybeAxiosError } from '../../../../common/api/utils';
import CopyTag from '../../../../common/components/copy-tag/CopyTag';
import { openLink } from '../../../../common/utils/linkUtils';
import * as Panel from '../../panel-utils/PanelUtils';
import { getWorksheetNames } from '../../../../../common/api/sheets';
import { maybeAxiosError } from '../../../../../common/api/utils';
import Button from '../../../../../common/components/buttons/Button';
import CopyTag from '../../../../../common/components/copy-tag/CopyTag';
import Input from '../../../../../common/components/input/input/Input';
import { openLink } from '../../../../../common/utils/linkUtils';
import * as Panel from '../../../panel-utils/PanelUtils';
import useGoogleSheet from './useGoogleSheet';
import { useSheetStore } from './useSheetStore';
@@ -138,50 +139,33 @@ export default function GSheetSetup(props: GSheetSetupProps) {
<Panel.Title>
Sync with Google Sheet (experimental)
{isAuthenticated ? (
<Button variant='ontime-subtle' size='sm' onClick={handleRevoke} isLoading={loading === 'cancel'}>
<Button onClick={handleRevoke} loading={loading === 'cancel'}>
Revoke Authentication
</Button>
) : (
<Button variant='ontime-subtle' size='sm' onClick={handleCancelFlow}>
Go Back
</Button>
<Button onClick={handleCancelFlow}>Go Back</Button>
)}
</Panel.Title>
<Panel.ListGroup>
<Panel.Description>Upload Client Secret provided by Google</Panel.Description>
<Panel.Error>{authenticationError}</Panel.Error>
<Input
type='file'
onChange={handleClientSecret}
accept='.json'
size='sm'
variant='ontime-filled'
isDisabled={isLoading || canAuthenticate}
/>
<Input fluid type='file' onChange={handleClientSecret} accept='.json' disabled={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'
fluid
placeholder='Sheet ID'
onChange={(event) => setSheetId(event.target.value)}
isDisabled={isLoading || canAuthenticate}
disabled={isLoading || canAuthenticate}
/>
</Panel.ListGroup>
{!canAuthenticate ? (
<Panel.ListGroup>
<Panel.InlineElements>
<Button
variant='ontime-subtle'
size='sm'
leftIcon={<IoCheckmark />}
onClick={handleConnect}
isDisabled={!canConnect || isLoading}
isLoading={loading === 'connect'}
>
<Button onClick={handleConnect} disabled={!canConnect || isLoading} loading={loading === 'connect'}>
<IoCheckmark />
Connect
</Button>
</Panel.InlineElements>
@@ -189,17 +173,12 @@ export default function GSheetSetup(props: GSheetSetupProps) {
) : (
<Panel.ListGroup>
<Panel.InlineElements>
{isAuthenticating && <Spinner />}
{isAuthenticating && <span>Authenticating...</span>}
<CopyTag copyValue={authKey ?? ''} 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}
>
<Button onClick={handleAuthenticate} disabled={!canAuthenticate}>
<IoShieldCheckmarkOutline />
Authenticate
</Button>
</Panel.InlineElements>
@@ -1,8 +1,8 @@
import { useState } from 'react';
import { Button } from '@chakra-ui/react';
import { CustomFields, Rundown } from 'ontime-types';
import * as Panel from '../../panel-utils/PanelUtils';
import Button from '../../../../../common/components/buttons/Button';
import * as Panel from '../../../panel-utils/PanelUtils';
import PreviewSpreadsheet from './preview/PreviewRundown';
import useGoogleSheet from './useGoogleSheet';
@@ -44,10 +44,10 @@ export default function ImportReview(props: ImportReviewProps) {
<Panel.Title>
Review Rundown
<Panel.InlineElements>
<Button onClick={handleCancel} variant='ontime-ghosted' size='sm' isDisabled={loading}>
<Button onClick={handleCancel} variant='ghosted' disabled={loading}>
Cancel
</Button>
<Button onClick={applyImport} variant='ontime-filled' size='sm' isLoading={loading}>
<Button onClick={applyImport} variant='primary' loading={loading}>
Apply
</Button>
</Panel.InlineElements>
@@ -1,17 +1,18 @@
import { ChangeEvent, useRef, useState } from 'react';
import { IoCloudOutline, IoDownloadOutline } from 'react-icons/io5';
import { Button, Input } from '@chakra-ui/react';
import { getErrorMessage, ImportMap } from 'ontime-utils';
import {
getWorksheetNames as getWorksheetNamesExcel,
importRundownPreview as importRundownPreviewExcel,
upload as uploadExcel,
} from '../../../../common/api/excel';
import { getWorksheetNames } from '../../../../common/api/sheets';
import { maybeAxiosError } from '../../../../common/api/utils';
import { validateExcelImport } from '../../../../common/utils/uploadUtils';
import * as Panel from '../../panel-utils/PanelUtils';
} from '../../../../../common/api/excel';
import { getWorksheetNames } from '../../../../../common/api/sheets';
import { maybeAxiosError } from '../../../../../common/api/utils';
import Button from '../../../../../common/components/buttons/Button';
import * as Editor from '../../../../../common/components/editor-utils/EditorUtils';
import { validateExcelImport } from '../../../../../common/utils/uploadUtils';
import * as Panel from '../../../panel-utils/PanelUtils';
import ImportMapForm from './import-map/ImportMapForm';
import GSheetInfo from './GSheetInfo';
@@ -154,87 +155,75 @@ export default function SourcesPanel() {
const showReview = rundown !== null && customFields !== null;
return (
<>
<Panel.Header>Data sources</Panel.Header>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Synchronise your rundown with an external source</Panel.SubHeader>
{error && <Panel.Error>{error}</Panel.Error>}
{showInput && (
<>
<GSheetInfo />
<Input
ref={fileInputRef}
style={{ display: 'none' }}
type='file'
onChange={handleFile}
accept='.xlsx'
data-testid='file-input'
/>
<div className={style.uploadSection}>
<div>
<Button
variant='ontime-filled'
size='sm'
leftIcon={<IoDownloadOutline />}
onClick={handleUpload}
isLoading={hasFile === 'loading'}
>
Import from spreadsheet
</Button>
<Panel.Description>Accepts .xlsx files</Panel.Description>
</div>
<div>
<Button
variant='ontime-filled'
size='sm'
leftIcon={<IoCloudOutline />}
onClick={openGSheetFlow}
isDisabled={hasFile !== 'none'}
>
Synchronise with Google
</Button>
<Panel.Description>Start authentication process</Panel.Description>
</div>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Synchronise your rundown with an external source</Panel.SubHeader>
{error && <Panel.Error>{error}</Panel.Error>}
{showInput && (
<>
<GSheetInfo />
<input
ref={fileInputRef}
style={{ display: 'none' }}
type='file'
onChange={handleFile}
accept='.xlsx'
data-testid='file-input'
/>
<div className={style.uploadSection}>
<div>
<Button variant='primary' onClick={handleUpload} loading={hasFile === 'loading'}>
<IoDownloadOutline />
Import from spreadsheet
</Button>
<Panel.Description>Accepts .xlsx files</Panel.Description>
</div>
<Editor.Separator orientation='vertical' />
<div>
<Button variant='primary' onClick={openGSheetFlow} disabled={hasFile !== 'none'}>
<IoCloudOutline />
Synchronise with Google
</Button>
<Panel.Description>Start authentication process</Panel.Description>
</div>
</>
)}
{showCompleted && (
<div className={style.finishSection}>
{error ? (
<span key='finish__error' className={style.error}>
Import failed
</span>
) : (
<span key='finish__success' className={style.success}>
Import successful
</span>
)}
<Button variant='ontime-filled' size='sm' onClick={resetFlow}>
Return
</Button>
</div>
)}
{showAuth && <GSheetSetup onCancel={cancelGSheetFlow} />}
{showImportMap && !showReview && (
<ImportMapForm
hasErrors={Boolean(error)}
isSpreadsheet={isExcelFlow}
onCancel={cancelImportMap}
onSubmitExport={handleSubmitExport}
onSubmitImport={handleSubmitImportPreview}
/>
)}
{showReview && (
<ImportReview
rundown={rundown}
customFields={customFields}
onFinished={handleFinished}
onCancel={cancelImportMap}
/>
)}
</Panel.Card>
</Panel.Section>
</>
</>
)}
{showCompleted && (
<div className={style.finishSection}>
{error ? (
<span key='finish__error' className={style.error}>
Import failed
</span>
) : (
<span key='finish__success' className={style.success}>
Import successful
</span>
)}
<Button variant='primary' onClick={resetFlow}>
Return
</Button>
</div>
)}
{showAuth && <GSheetSetup onCancel={cancelGSheetFlow} />}
{showImportMap && !showReview && (
<ImportMapForm
hasErrors={Boolean(error)}
isSpreadsheet={isExcelFlow}
onCancel={cancelImportMap}
onSubmitExport={handleSubmitExport}
onSubmitImport={handleSubmitImportPreview}
/>
)}
{showReview && (
<ImportReview
rundown={rundown}
customFields={customFields}
onFinished={handleFinished}
onCancel={cancelImportMap}
/>
)}
</Panel.Card>
</Panel.Section>
);
}
@@ -1,10 +1,13 @@
import { useEffect, useState } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { IoAdd, IoTrash } from 'react-icons/io5';
import { Button, IconButton, Input, Select, Tooltip } from '@chakra-ui/react';
import { Select, Tooltip } from '@chakra-ui/react';
import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils';
import * as Panel from '../../../panel-utils/PanelUtils';
import Button from '../../../../../../common/components/buttons/Button';
import IconButton from '../../../../../../common/components/buttons/IconButton';
import Input from '../../../../../../common/components/input/input/Input';
import * as Panel from '../../../../panel-utils/PanelUtils';
import useGoogleSheet from '../useGoogleSheet';
import { useSheetStore } from '../useSheetStore';
@@ -95,31 +98,29 @@ export default function ImportMapForm(props: ImportMapFormProps) {
<Panel.InlineElements>
{!isSpreadsheet && (
<Tooltip label='Revoke the google authentication'>
<Button variant='ontime-subtle' size='sm' onClick={handleRevoke} isDisabled={isLoading}>
<Button onClick={handleRevoke} disabled={isLoading}>
Revoke
</Button>
</Tooltip>
)}
<Button variant='ontime-subtle' size='sm' onClick={onCancel} isDisabled={isLoading}>
<Button onClick={onCancel} disabled={isLoading}>
Cancel
</Button>
{!isSpreadsheet && (
<Button
variant='ontime-filled'
size='sm'
variant='primary'
onClick={handleSubmit(handleExport)}
isDisabled={!canSubmitGSheet}
isLoading={loading === 'export'}
disabled={!canSubmitGSheet}
loading={loading === 'export'}
>
Export
</Button>
)}
<Button
variant='ontime-filled'
size='sm'
variant='primary'
onClick={handleSubmit(handleImportPreview)}
isDisabled={!canSubmit}
isLoading={loading === 'import'}
disabled={!canSubmit}
loading={loading === 'import'}
>
Import preview
</Button>
@@ -168,9 +169,7 @@ export default function ImportMapForm(props: ImportMapFormProps) {
<td>
<Input
id={importName as string}
size='sm'
variant='ontime-filled'
autoComplete='off'
fluid
maxLength={25}
defaultValue={importName as string}
placeholder='Use default column name'
@@ -190,10 +189,8 @@ export default function ImportMapForm(props: ImportMapFormProps) {
<tr key={key}>
<td>
<Input
size='sm'
variant='ontime-filled'
autoComplete='off'
maxLength={25}
fluid
defaultValue={ontimeName}
placeholder='Name of the field as shown in Ontime'
{...register(`custom.${index}.ontimeName`, {
@@ -208,10 +205,8 @@ export default function ImportMapForm(props: ImportMapFormProps) {
</td>
<td>
<Input
size='sm'
variant='ontime-filled'
autoComplete='off'
maxLength={25}
fluid
defaultValue={importName}
placeholder='Name of the column in the spreadsheet'
{...register(`custom.${index}.importName`)}
@@ -219,13 +214,12 @@ export default function ImportMapForm(props: ImportMapFormProps) {
</td>
<td className={style.singleActionCell}>
<IconButton
size='sm'
variant='ontime-ghosted'
color='#FA5656' // $red-500
icon={<IoTrash />}
variant='ghosted-destructive'
aria-label='Delete entry'
onClick={() => deleteCustomImport(index)}
/>
>
<IoTrash />
</IconButton>
</td>
</tr>
);
@@ -233,8 +227,8 @@ export default function ImportMapForm(props: ImportMapFormProps) {
<tr>
<td />
<Panel.InlineElements as='td' align='end'>
<Button size='sm' variant='ontime-subtle' rightIcon={<IoAdd />} onClick={addCustomImport}>
Add custom field
<Button onClick={addCustomImport}>
Add custom field <IoAdd />
</Button>
</Panel.InlineElements>
<td />
@@ -3,9 +3,9 @@ import { IoLink } from 'react-icons/io5';
import { CustomFields, isOntimeBlock, isOntimeEvent, Rundown } 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 '../../../panel-utils/PanelUtils';
import Tag from '../../../../../../common/components/tag/Tag';
import { getAccessibleColour } from '../../../../../../common/utils/styleUtils';
import * as Panel from '../../../../panel-utils/PanelUtils';
import style from './PreviewRundown.module.scss';
@@ -2,16 +2,16 @@ import { useQueryClient } from '@tanstack/react-query';
import { AuthenticationStatus, CustomFields, ProjectRundowns } from 'ontime-types';
import { ImportMap } from 'ontime-utils';
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/constants';
import { patchData } from '../../../../common/api/db';
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../../common/api/constants';
import { patchData } from '../../../../../common/api/db';
import {
previewRundown,
requestConnection,
revokeAuthentication,
uploadRundown,
verifyAuthenticationStatus,
} from '../../../../common/api/sheets';
import { maybeAxiosError } from '../../../../common/api/utils';
} from '../../../../../common/api/sheets';
import { maybeAxiosError } from '../../../../../common/api/utils';
import { useSheetStore } from './useSheetStore';
@@ -4,17 +4,14 @@ import { MessageTag } from 'ontime-types';
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import { usePing } from '../../../../common/hooks/useSocket';
import { sendSocket } from '../../../../common/utils/socket';
import { isDockerImage, isOntimeCloud } from '../../../../externals';
import { isDockerImage } from '../../../../externals';
import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils';
import ClientControlPanel from '../client-control-panel/ClientControlPanel';
import GenerateLinkFormExport from './GenerateLinkFormExport';
import InfoNif from './NetworkInterfaces';
import ClientControlPanel from './client-control/ClientControlPanel';
import LogExport from './NetworkLogExport';
export default function NetworkLogPanel({ location }: PanelBaseProps) {
const linkRef = useScrollIntoView<HTMLDivElement>('link', location);
const clientsRef = useScrollIntoView<HTMLDivElement>('clients', location);
const logRef = useScrollIntoView<HTMLDivElement>('log', location);
@@ -26,21 +23,6 @@ export default function NetworkLogPanel({ location }: PanelBaseProps) {
<OntimeCloudStats />
</Panel.Section>
)}
<div ref={linkRef}>
<Panel.Section>
<Panel.Card>
<Panel.SubHeader>Share Ontime Link</Panel.SubHeader>
<Panel.Divider />
{!isOntimeCloud && (
<>
<Panel.Paragraph>Ontime is streaming on the following network interfaces</Panel.Paragraph>
<InfoNif />
</>
)}
<GenerateLinkFormExport />
</Panel.Card>
</Panel.Section>
</div>
<div ref={logRef}>
<LogExport />
</div>
@@ -1,4 +1,4 @@
import * as Panel from '../../panel-utils/PanelUtils';
import * as Panel from '../../../panel-utils/PanelUtils';
import ClientList from './ClientList';
@@ -1,32 +1,34 @@
import { useState } from 'react';
import { Badge, Button, useDisclosure } from '@chakra-ui/react';
import { useDisclosure } from '@mantine/hooks';
import { Client } from 'ontime-types';
import { RedirectClientModal } from '../../../../common/components/client-modal/RedirectClientModal';
import { RenameClientModal } from '../../../../common/components/client-modal/RenameClientModal';
import { setClientRemote } from '../../../../common/hooks/useSocket';
import { useClientStore } from '../../../../common/stores/clientStore';
import * as Panel from '../../panel-utils/PanelUtils';
import Button from '../../../../../common/components/buttons/Button';
import { RedirectClientModal } from '../../../../../common/components/client-modal/RedirectClientModal';
import { RenameClientModal } from '../../../../../common/components/client-modal/RenameClientModal';
import Tag from '../../../../../common/components/tag/Tag';
import { setClientRemote } from '../../../../../common/hooks/useSocket';
import { useClientStore } from '../../../../../common/stores/clientStore';
import * as Panel from '../../../panel-utils/PanelUtils';
import style from './ClientControlPanel.module.scss';
export default function ClientList() {
const id = useClientStore((store) => store.id);
const clients = useClientStore((store) => store.clients);
const { isOpen: isOpenRedirect, onOpen: onOpenRedirect, onClose: onCloseRedirect } = useDisclosure();
const { isOpen: isOpenRename, onOpen: onOpenRename, onClose: onCloseRename } = useDisclosure();
const [isOpenRedirect, redirectHandler] = useDisclosure();
const [isOpenRename, renameHandler] = useDisclosure();
const { setIdentify } = setClientRemote;
const [targetId, setTargetId] = useState('');
const openRename = (targetId: string) => {
setTargetId(targetId);
onOpenRename();
renameHandler.open();
};
const openRedirect = (targetId: string) => {
setTargetId(targetId);
onOpenRedirect();
redirectHandler.open();
};
const ontimeClients = Object.entries(clients).filter(([_, { type }]) => type === 'ontime');
@@ -43,11 +45,16 @@ export default function ClientList() {
origin={targetClient.origin}
currentPath={targetClient.path}
isOpen={isOpenRedirect}
onClose={onCloseRedirect}
onClose={redirectHandler.close}
/>
)}
{isOpenRename && (
<RenameClientModal id={targetId} name={targetClient?.name} isOpen={isOpenRename} onClose={onCloseRename} />
<RenameClientModal
id={targetId}
name={targetClient?.name}
isOpen={isOpenRename}
onClose={renameHandler.close}
/>
)}
<Panel.Section>
<Panel.Title>Ontime Clients ({ontimeClients.length})</Panel.Title>
@@ -66,20 +73,16 @@ export default function ClientList() {
return (
<tr key={key}>
<Panel.InlineElements relation='inner' as='td'>
{isCurrent && (
<Badge variant='outline' colorScheme='yellow' size='xs'>
self
</Badge>
)}
{isCurrent && <Tag>SELF</Tag>}
{name}
</Panel.InlineElements>
<td>{path}</td>
<Panel.InlineElements relation='inner'>
<Button
size='xs'
size='small'
className={`${identify ? style.blink : ''}`}
isDisabled={isCurrent}
variant={identify ? 'ontime-filled' : 'ontime-subtle'}
disabled={isCurrent}
variant={identify ? 'primary' : 'subtle'}
data-testid={isCurrent ? '' : 'not-self-identify'}
onClick={() => {
setIdentify({ target: key, identify: !identify });
@@ -88,8 +91,7 @@ export default function ClientList() {
Identify
</Button>
<Button
size='xs'
variant='ontime-subtle'
size='small'
data-testid={isCurrent ? '' : 'not-self-rename'}
onClick={() => openRename(key)}
>
@@ -97,9 +99,8 @@ export default function ClientList() {
</Button>
<Button
size='xs'
variant='ontime-subtle'
isDisabled={isCurrent}
size='small'
disabled={isCurrent}
data-testid={isCurrent ? '' : 'not-self-redirect'}
onClick={() => openRedirect(key)}
>
@@ -1,7 +1,8 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input } from '@chakra-ui/react';
import Button from '../../../../common/components/buttons/Button';
import Input from '../../../../common/components/input/input/Input';
import { preventEscape } from '../../../../common/utils/keyEvent';
import * as Panel from '../../panel-utils/PanelUtils';
@@ -45,21 +46,16 @@ export default function ProjectForm({ action, filename, onSubmit, onCancel }: Pr
<Input
className={style.formInput}
id='filename'
size='sm'
type='text'
variant='ontime-filled'
placeholder='Enter new name'
autoComplete='off'
{...register('filename', { required: true })}
/>
<Panel.InlineElements relation='inner'>
<Button onClick={onCancel} size='sm' variant='ontime-ghosted' disabled={isSubmitting}>
<Button onClick={onCancel} variant='ghosted' disabled={isSubmitting}>
Cancel
</Button>
<Button
size='sm'
variant='ontime-filled'
isDisabled={!isDirty || !isValid || isSubmitting}
variant='primary'
disabled={!isDirty || !isValid || isSubmitting}
type='submit'
className={style.saveButton}
>
@@ -1,5 +1,6 @@
import { useState } from 'react';
import Info from '../../../../common/components/info/Info';
import { useOrderedProjectList } from '../../../../common/hooks-query/useProjectList';
import * as Panel from '../../panel-utils/PanelUtils';
@@ -8,7 +9,7 @@ import ProjectListItem, { EditMode } from './ProjectListItem';
import style from './ProjectPanel.module.scss';
export default function ProjectList() {
const { data, refetch } = useOrderedProjectList();
const { data, refetch, status } = useOrderedProjectList();
const [editingMode, setEditingMode] = useState<EditMode | null>(null);
const [editingFilename, setEditingFilename] = useState<string | null>(null);
@@ -27,30 +28,47 @@ export default function ProjectList() {
await refetch();
};
if (status === 'pending') {
return (
<div className={style.empty}>
<Panel.Loader isLoading />
</div>
);
}
const numProjects = data.reorderedProjectFiles.length;
return (
<Panel.Table>
<thead>
<tr>
<th className={style.containCell}>File Name</th>
<th>Last Used</th>
<th />
</tr>
</thead>
<tbody>
{data.reorderedProjectFiles.map((project) => (
<ProjectListItem
key={project.filename}
filename={project.filename}
updatedAt={project.updatedAt}
onToggleEditMode={handleToggleEditMode}
onSubmit={handleClear}
onRefetch={handleRefetch}
editingFilename={editingFilename}
editingMode={editingMode}
current={project.filename === data.lastLoadedProject}
/>
))}
</tbody>
</Panel.Table>
<>
{numProjects > 20 && (
<Info className={style.warningInfo} type='warning'>
You have {numProjects} projects. Consider deleting unused projects to improve performance.
</Info>
)}
<Panel.Table>
<thead>
<tr>
<th className={style.containCell}>File Name</th>
<th>Last Used</th>
<th />
</tr>
</thead>
<tbody>
{data.reorderedProjectFiles.map((project) => (
<ProjectListItem
key={project.filename}
filename={project.filename}
updatedAt={project.updatedAt}
onToggleEditMode={handleToggleEditMode}
onSubmit={handleClear}
onRefetch={handleRefetch}
editingFilename={editingFilename}
editingMode={editingMode}
current={project.filename === data.lastLoadedProject}
/>
))}
</tbody>
</Panel.Table>
</>
);
}
@@ -1,11 +1,12 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Switch } from '@chakra-ui/react';
import { Switch } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { PROJECT_DATA } from '../../../../common/api/constants';
import { getDb, patchData } from '../../../../common/api/db';
import { maybeAxiosError } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import { cx } from '../../../../common/utils/styleUtils';
import * as Panel from '../../panel-utils/PanelUtils';
@@ -76,16 +77,10 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
<Panel.Title>
Merge {`"${fileName}"`}
<Panel.InlineElements>
<Button onClick={onClose} variant='ontime-ghosted' size='sm' isDisabled={isSubmitting}>
<Button onClick={onClose} variant='ghosted' disabled={isSubmitting}>
Cancel
</Button>
<Button
isDisabled={!isValid || !isDirty}
type='submit'
isLoading={isSubmitting}
variant='ontime-filled'
size='sm'
>
<Button type='submit' disabled={!isValid || !isDirty} loading={isSubmitting} variant='primary'>
Merge
</Button>
</Panel.InlineElements>
@@ -45,36 +45,11 @@
}
}
.uploadLogoCard {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: $gray-1350;
border: 1px solid $white-10;
border-radius: 3px;
img {
max-width: 250px;
height: auto;
}
.warningInfo {
margin-bottom: 1rem;
}
.customDataItem {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.5rem;
.titleRow{
display: flex;
gap: 1rem;
align-items: end;
label {
flex: 1;
}
}
}
.empty {
height: 300px;
position: relative;
}
@@ -5,16 +5,12 @@ import QuickStart from '../../quick-start/QuickStart';
import type { SettingsOptionId } from '../../useAppSettingsMenu';
import ManageProjects from './ManageProjects';
import ManageRundowns from './ManageRundowns';
import ProjectData from './ProjectData';
interface ProjectPanelProps extends PanelBaseProps {
setLocation: (location: SettingsOptionId) => void;
}
export default function ProjectPanel({ location, setLocation }: ProjectPanelProps) {
const projectRef = useScrollIntoView<HTMLDivElement>('data', location);
const manageRundownsRef = useScrollIntoView<HTMLDivElement>('rundowns', location);
const manageProjectsRef = useScrollIntoView<HTMLDivElement>('list', location);
const handleQuickClose = () => {
@@ -25,12 +21,6 @@ export default function ProjectPanel({ location, setLocation }: ProjectPanelProp
<>
<Panel.Header>Project</Panel.Header>
<QuickStart isOpen={location === 'create'} onClose={handleQuickClose} />
<div ref={projectRef}>
<ProjectData />
</div>
<div ref={manageRundownsRef}>
<ManageRundowns />
</div>
<div ref={manageProjectsRef}>
<ManageProjects />
</div>
@@ -1,19 +1,21 @@
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { Button, Input, Select } from '@chakra-ui/react';
import { Select } from '@chakra-ui/react';
import { Settings } from 'ontime-types';
import { postSettings } from '../../../../common/api/settings';
import { maybeAxiosError } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import Input from '../../../../common/components/input/input/Input';
import useSettings from '../../../../common/hooks-query/useSettings';
import { preventEscape } from '../../../../common/utils/keyEvent';
import { isOnlyNumbers } from '../../../../common/utils/regex';
import { isOntimeCloud } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
import GeneralPinInput from './GeneralPinInput';
import GeneralPinInput from './composite/GeneralPinInput';
export default function GeneralPanelForm() {
export default function GeneralSettings() {
const { data, status, refetch } = useSettings();
const {
handleSubmit,
@@ -69,17 +71,10 @@ export default function GeneralPanelForm() {
<Panel.SubHeader>
General settings
<Panel.InlineElements>
<Button isDisabled={!isDirty || isSubmitting} variant='ontime-ghosted' size='sm' onClick={onReset}>
<Button disabled={!isDirty || isSubmitting} variant='ghosted' onClick={onReset}>
Revert to saved
</Button>
<Button
type='submit'
form='app-settings'
isLoading={isSubmitting}
isDisabled={disableSubmit}
variant='ontime-filled'
size='sm'
>
<Button type='submit' form='app-settings' loading={isSubmitting} disabled={disableSubmit} variant='primary'>
Save
</Button>
</Panel.InlineElements>
@@ -101,12 +96,10 @@ export default function GeneralPanelForm() {
/>
<Input
id='serverPort'
size='sm'
type='number'
variant='ontime-filled'
maxLength={5}
width='75px'
isDisabled={isOntimeCloud}
style={{ width: '75px' }}
disabled={isOntimeCloud}
{...register('serverPort', {
required: { value: true, message: 'Required field' },
max: { value: 65535, message: 'Port must be within range 1024 - 65535' },
@@ -15,7 +15,7 @@ import { validateLogo } from '../../../../common/utils/uploadUtils';
import { documentationUrl } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
import style from './ProjectPanel.module.scss';
import style from './SettingsPanel.module.scss';
export default function ProjectData() {
const { data, status, refetch } = useProjectData();
@@ -0,0 +1,33 @@
.uploadLogoCard {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
flex-direction: column;
background-color: $gray-1350;
border: 1px solid $white-10;
border-radius: 3px;
img {
max-width: 250px;
height: auto;
}
}
.customDataItem {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.titleRow {
display: flex;
gap: 1rem;
align-items: end;
label {
flex: 1;
}
}
@@ -0,0 +1,28 @@
import useScrollIntoView from '../../../../common/hooks/useScrollIntoView';
import type { PanelBaseProps } from '../../panel-list/PanelList';
import * as Panel from '../../panel-utils/PanelUtils';
import GeneralSettings from './GeneralSettings';
import ProjectData from './ProjectData';
import ViewSettings from './ViewSettings';
export default function SettingsPanel({ location }: PanelBaseProps) {
const dataRef = useScrollIntoView<HTMLDivElement>('data', location);
const generalRef = useScrollIntoView<HTMLDivElement>('general', location);
const viewRef = useScrollIntoView<HTMLDivElement>('view', location);
return (
<>
<Panel.Header>Settings</Panel.Header>
<div ref={dataRef}>
<ProjectData />
</div>
<div ref={generalRef}>
<GeneralSettings />
</div>
<div ref={viewRef}>
<ViewSettings />
</div>
</>
);
}
@@ -1,26 +1,26 @@
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Button, Input, Switch, useDisclosure } from '@chakra-ui/react';
import { ViewSettings } from 'ontime-types';
import { Switch } from '@chakra-ui/react';
import { useDisclosure } from '@mantine/hooks';
import { ViewSettings as ViewSettingsType } from 'ontime-types';
import { maybeAxiosError } from '../../../../common/api/utils';
import Button from '../../../../common/components/buttons/Button';
import Info from '../../../../common/components/info/Info';
import { SwatchPickerRHF } from '../../../../common/components/input/colour-input/SwatchPicker';
import Input from '../../../../common/components/input/input/Input';
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
import useInfo from '../../../../common/hooks-query/useInfo';
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
import { preventEscape } from '../../../../common/utils/keyEvent';
import { isOntimeCloud } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
import CodeEditorModal from './StyleEditorModal';
import CodeEditorModal from './composite/StyleEditorModal';
const cssOverrideDocsUrl = 'https://docs.getontime.no/features/custom-styling/';
export default function ViewSettingsForm() {
export default function ViewSettings() {
const { data, isPending, mutateAsync } = useViewSettings();
const { data: info, status: infoStatus } = useInfo();
const { isOpen: isCodeEditorOpen, onOpen: onCodeEditorOpen, onClose: onCodeEditorClose } = useDisclosure();
const [isCodeEditorOpen, codeEditorHandler] = useDisclosure();
const {
control,
@@ -29,7 +29,7 @@ export default function ViewSettingsForm() {
register,
reset,
formState: { isSubmitting, isDirty, errors },
} = useForm<ViewSettings>({
} = useForm<ViewSettingsType>({
defaultValues: data,
values: data,
resetOptions: {
@@ -44,7 +44,7 @@ export default function ViewSettingsForm() {
}
}, [data, reset]);
const onSubmit = async (formData: ViewSettings) => {
const onSubmit = async (formData: ViewSettingsType) => {
try {
mutateAsync(formData);
} catch (error) {
@@ -61,8 +61,6 @@ export default function ViewSettingsForm() {
return null;
}
const isLoading = isPending || infoStatus === 'pending';
return (
<Panel.Section
as='form'
@@ -74,33 +72,25 @@ export default function ViewSettingsForm() {
<Panel.SubHeader>
View settings
<Panel.InlineElements>
<Button isDisabled={!isDirty} variant='ontime-ghosted' size='sm' onClick={onReset}>
<Button disabled={!isDirty} variant='ghosted' onClick={onReset}>
Revert to saved
</Button>
<Button type='submit' isLoading={isSubmitting} isDisabled={!isDirty} variant='ontime-filled' size='sm'>
<Button type='submit' loading={isSubmitting} disabled={!isDirty} variant='primary'>
Save
</Button>
</Panel.InlineElements>
</Panel.SubHeader>
<Panel.Divider />
<Info>
You can the Ontime views or customise its styles by modifying the provided CSS file.
You can customise the styles applied to Ontime views by providing overriding CSS rules.
<br />
{!isOntimeCloud && (
<>
<br />
The loaded CSS file is in the user directory at{' '}
<Panel.BlockQuote>{`${info.publicDir}/user/styles/override.css`}</Panel.BlockQuote>
<br />
</>
)}
<ExternalLink href={cssOverrideDocsUrl}>See the docs</ExternalLink>
</Info>
<Panel.Section>
<Panel.Loader isLoading={isLoading} />
<Panel.Loader isLoading={isPending} />
<Panel.Error>{errors.root?.message}</Panel.Error>
<Panel.ListGroup>
<CodeEditorModal isOpen={isCodeEditorOpen} onClose={onCodeEditorClose} />
<CodeEditorModal isOpen={isCodeEditorOpen} onClose={codeEditorHandler.close} />
<Panel.ListItem>
<Panel.Field
title='Override CSS styles'
@@ -113,13 +103,7 @@ export default function ViewSettingsForm() {
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
)}
/>
<Button
onClick={onCodeEditorOpen}
variant='ontime-subtle'
size='sm'
isDisabled={isSubmitting}
width='fit-content'
>
<Button onClick={codeEditorHandler.open} disabled={isSubmitting}>
Edit CSS override
</Button>
</Panel.ListItem>
@@ -158,11 +142,8 @@ export default function ViewSettingsForm() {
description='Message for negative timers; applies only if the timer isn`t frozen on End. If no message is provided, it continues into negative time'
/>
<Input
size='sm'
autoComplete='off'
variant='ontime-filled'
maxLength={150}
width='275px'
style={{ width: '275px' }}
placeholder='Shown when timer reaches end'
{...register('endMessage')}
/>
@@ -4,7 +4,7 @@ import { IoEyeOutline } from 'react-icons/io5';
import { IconButton, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
import { Settings } from 'ontime-types';
import { isAlphanumeric } from '../../../../common/utils/regex';
import { isAlphanumeric } from '../../../../../common/utils/regex';
interface GeneralPinInputProps {
register: UseFormRegister<Settings>;
@@ -1,10 +1,10 @@
import { lazy, useEffect, useRef, useState } from 'react';
import { getCSSContents, postCSSContents, restoreCSSContents } from '../../../../common/api/assets';
import Button from '../../../../common/components/buttons/Button';
import Info from '../../../../common/components/info/Info';
import Modal from '../../../../common/components/modal/Modal';
import * as Panel from '../../panel-utils/PanelUtils';
import { getCSSContents, postCSSContents, restoreCSSContents } from '../../../../../common/api/assets';
import Button from '../../../../../common/components/buttons/Button';
import Info from '../../../../../common/components/info/Info';
import Modal from '../../../../../common/components/modal/Modal';
import * as Panel from '../../../panel-utils/PanelUtils';
import style from './StyleEditorModal.module.scss';
@@ -1,27 +1,20 @@
import { useRef } from 'react';
import {
AlertDialog,
AlertDialogBody,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogOverlay,
Button,
useDisclosure,
} from '@chakra-ui/react';
import { useDisclosure } from '@mantine/hooks';
import Button from '../../../../common/components/buttons/Button';
import Dialog from '../../../../common/components/dialog/Dialog';
import { useElectronEvent } from '../../../../common/hooks/useElectronEvent';
import { isLocalhost, isOntimeCloud } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
export default function ShutdownPanel() {
const { isElectron, sendToElectron } = useElectronEvent();
const { isOpen, onOpen, onClose } = useDisclosure();
const [isOpen, handler] = useDisclosure();
const cancelRef = useRef<HTMLButtonElement | null>(null);
const sendShutdown = () => {
sendToElectron('shutdown', 'now');
onClose();
handler.close();
};
const canShutdown = isElectron || isLocalhost;
@@ -40,32 +33,33 @@ export default function ShutdownPanel() {
The runtime state will be lost, but your project is kept for next time.
</Panel.Paragraph>
)}
<Button colorScheme='red' onClick={onOpen} maxWidth='350px' isDisabled={!(isElectron || isLocalhost)}>
<Button variant='destructive' onClick={handler.open} disabled={!(isElectron || isLocalhost)}>
Shutdown ontime
</Button>
{!canShutdown && (
<Panel.Description>Note: Ontime can only be shutdown from the machine it is running in.</Panel.Description>
)}
<AlertDialog variant='ontime' isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
<AlertDialogOverlay>
<AlertDialogContent>
<AlertDialogHeader fontSize='lg' fontWeight='bold'>
Ontime Shutdown
</AlertDialogHeader>
<AlertDialogBody>
This will shutdown the Ontime server. <br /> Are you sure?
</AlertDialogBody>
<AlertDialogFooter>
<Button ref={cancelRef} onClick={onClose} variant='ontime-ghosted-white'>
Cancel
</Button>
<Button colorScheme='red' onClick={sendShutdown} disabled={!canShutdown}>
Shutdown
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialogOverlay>
</AlertDialog>
<Dialog
isOpen={isOpen}
title='Shutdown Ontime'
showCloseButton
onClose={handler.close}
bodyElements={
<Panel.Paragraph>
This will shutdown the Ontime server. <br /> Are you sure?
</Panel.Paragraph>
}
footerElements={
<>
<Button ref={cancelRef} onClick={handler.close} variant='ghosted-white'>
Cancel
</Button>
<Button variant='destructive' onClick={sendShutdown} disabled={!canShutdown}>
Shutdown
</Button>
</>
}
/>
</Panel.Section>
</>
);
@@ -11,61 +11,63 @@ export type SettingsOption = {
};
const staticOptions = [
{
id: 'settings',
label: 'Settings',
secondary: [
{ id: 'settings__data', label: 'Project data' },
{ id: 'settings__general', label: 'General settings' },
{ id: 'settings__view', label: 'View settings' },
],
},
{
id: 'project',
label: 'Project',
split: true,
secondary: [
{ id: 'project__create', label: 'Create...' },
{ id: 'project__data', label: 'Project data' },
{ id: 'project__rundowns', label: 'Manage rundowns' },
{ id: 'project__list', label: 'Manage projects' },
],
},
{
id: 'general',
label: 'App Settings',
id: 'manage',
label: 'Project data',
secondary: [
{ id: 'general__settings', label: 'General settings' },
{ id: 'general__editor', label: 'Editor settings' },
{ id: 'general__view', label: 'View settings' },
{ id: 'manage__defaults', label: 'Rundown defaults' },
{ id: 'manage__custom', label: 'Custom fields' },
{ id: 'manage__rundowns', label: 'Manage rundowns' },
{ id: 'manage__sheets', label: 'Import spreadsheet' },
{ id: 'manage__sheets', label: 'Sync with Google Sheet' },
],
},
{
id: 'feature_settings',
label: 'Feature Settings',
secondary: [
{ id: 'feature_settings__custom', label: 'Custom fields' },
{ id: 'feature_settings__urlpresets', label: 'URL Presets' },
{ id: 'feature_settings__report', label: 'Report' },
],
},
{
id: 'sources',
label: 'Data Sources',
secondary: [
{ id: 'sources__xlsx', label: 'Import spreadsheet' },
{ id: 'sources__gsheet', label: 'Sync with Google Sheet' },
],
split: true,
},
{
id: 'automation',
label: 'Automation',
split: true,
secondary: [
{ id: 'automation__settings', label: 'Automation settings' },
{ id: 'automation__automations', label: 'Manage automations' },
{ id: 'automation__triggers', label: 'Manage triggers' },
],
},
{
id: 'sharing',
label: 'Sharing and reporting',
split: true,
secondary: [
{ id: 'sharing__presets', label: 'URL Presets' },
{
id: 'sharing__link',
label: 'Share link',
},
{ id: 'sharing__report', label: 'Runtime report' },
],
},
{
id: 'network',
label: 'Network',
split: true,
secondary: [
{
id: 'network__link',
label: 'Share link',
},
{
id: 'network__log',
label: 'Event log',
@@ -1,6 +1,6 @@
import { PropsWithChildren, useEffect, useRef, useState } from 'react';
import { Input } from '@chakra-ui/react';
import Input from '../../../common/components/input/input/Input';
import { cx } from '../../../common/utils/styleUtils';
import style from './InputRow.module.scss';
@@ -47,15 +47,7 @@ export default function InputRow(props: PropsWithChildren<InputRowProps>) {
{label}
</label>
<div className={style.inputItems}>
<Input
id={label}
ref={inputRef}
size='sm'
variant='ontime-filled'
value={value}
onChange={handleInputChange}
placeholder={placeholder}
/>
<Input id={label} ref={inputRef} value={value} onChange={handleInputChange} placeholder={placeholder} />
{children}
</div>
</div>
@@ -1,11 +1,11 @@
import { Fragment, useRef, useState } from 'react';
import { IoClose } from 'react-icons/io5';
import { Dialog } from '@base-ui-components/react/dialog';
import { Textarea } from '@chakra-ui/react';
import { OntimeEvent } from 'ontime-types';
import Button from '../../../common/components/buttons/Button';
import IconButton from '../../../common/components/buttons/IconButton';
import Textarea from '../../../common/components/input/textarea/Textarea';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import { EditEvent } from '../operator.types';
@@ -74,11 +74,10 @@ export default function EditModal(props: EditModalProps) {
ref={(element) => {
if (element) inputRef.current.push(element);
}}
variant='ontime-filled'
placeholder={`Add value for ${field.label} field`}
defaultValue={field.value}
data-field={field.id}
isDisabled={loading}
disabled={loading}
resize='none'
rows={5}
/>
+2 -2
View File
@@ -435,9 +435,9 @@ export default function Rundown({ data }: RundownProps) {
* Outside a block, the value will be undefined
* If the colour is empty string ''
* ie: we are inside a block, but there is no defined colour
* we default to $gray-1050 #303030
* we default to $gray-500 #9d9d9d
*/
const blockColour = rundownMetadata.groupColour === '' ? '#303030' : rundownMetadata.groupColour;
const blockColour = rundownMetadata.groupColour === '' ? '#9d9d9d' : rundownMetadata.groupColour;
return (
<Fragment key={entry.id}>
@@ -1,6 +1,6 @@
import { useCallback, useRef } from 'react';
import { Input } from '@chakra-ui/react';
import Input from '../../../common/components/input/input/Input';
import useReactiveTextInput from '../../../common/components/input/text-input/useReactiveTextInput';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import { cx } from '../../../common/utils/styleUtils';
@@ -39,7 +39,8 @@ export default function EditableBlockTitle(props: TitleEditorProps) {
return (
<Input
data-testid='block__title'
variant='ontime-ghosted'
variant='ghosted'
fluid
ref={ref}
value={value}
className={classes}
@@ -47,12 +48,6 @@ export default function EditableBlockTitle(props: TitleEditorProps) {
onChange={onChange}
onBlur={onBlur}
onKeyDown={onKeyDown}
autoComplete='off'
fontWeight='600'
letterSpacing='0.25px'
paddingLeft='0'
size='sm'
fontSize='1rem'
/>
);
}
@@ -1,21 +1,20 @@
import { memo } from 'react';
import { useDisclosure } from '@chakra-ui/react';
import { useHotkeys } from '@mantine/hooks';
import { useDisclosure, useHotkeys } from '@mantine/hooks';
import Finder from '../../../views/editor/finder/Finder';
export default memo(FinderPlacement);
function FinderPlacement() {
const { isOpen, onToggle, onClose } = useDisclosure();
const [isOpen, handler] = useDisclosure();
useHotkeys([
['mod + f', onToggle],
['Escape', onClose],
['mod + f', handler.toggle],
['Escape', handler.close],
]);
if (isOpen) {
return <Finder isOpen={isOpen} onClose={onClose} />;
return <Finder isOpen={isOpen} onClose={handler.close} />;
}
return null;
@@ -59,9 +59,9 @@ function QuickAddBlock({ previousEventId, parentBlock, backgroundColor }: QuickA
/**
* If the colour is empty string ''
* ie: we are inside a block, but there is no defined colour
* we default to $gray-1050 #303030
* we default to $gray-500 #9d9d9d
*/
const blockColour = backgroundColor === '' ? '#303030' : backgroundColor;
const blockColour = backgroundColor === '' ? '#9d9d9d' : backgroundColor;
return (
<div className={style.quickAdd} style={blockColour ? { '--user-bg': blockColour } : {}}>
@@ -51,6 +51,7 @@
display: flex;
gap: 3rem;
margin-bottom: 0.25rem;
white-space: nowrap;
}
.metaEntry {
@@ -7,11 +7,11 @@ import {
IoReorderTwo,
IoTrash,
} from 'react-icons/io5';
import { IconButton } from '@chakra-ui/react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { EntryId, OntimeBlock } from 'ontime-types';
import IconButton from '../../../common/components/buttons/IconButton';
import { useContextMenu } from '../../../common/hooks/useContextMenu';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import { cx, getAccessibleColour } from '../../../common/utils/styleUtils';
@@ -120,13 +120,7 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }:
<div className={style.header}>
<div className={style.titleRow}>
<EditableBlockTitle title={data.title} eventId={data.id} placeholder='Block title' />
<IconButton
aria-label='Collapse'
onClick={() => onCollapse(!collapsed, data.id)}
color='#e2e2e2' // $gray-200
variant='ontime-ghosted'
size='sm'
>
<IconButton aria-label='Collapse' variant='subtle-white' onClick={() => onCollapse(!collapsed, data.id)}>
{collapsed ? <IoChevronUp /> : <IoChevronDown />}
</IconButton>
</div>
@@ -1,10 +1,10 @@
import { useEffect, useRef } from 'react';
import { IoCheckmarkDone, IoClose, IoReorderTwo } from 'react-icons/io5';
import { Button } from '@chakra-ui/react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { OntimeDelay } from 'ontime-types';
import Button from '../../../common/components/buttons/Button';
import DelayInput from '../../../common/components/input/delay-input/DelayInput';
import { useEntryActions } from '../../../common/hooks/useEntryAction';
import { cx } from '../../../common/utils/styleUtils';
@@ -64,10 +64,11 @@ export default function RundownDelay({ data, hasCursor }: RundownDelayProps) {
</span>
<DelayInput eventId={data.id} duration={data.duration} />
<div className={style.actionButtons}>
<Button onClick={applyDelayHandler} size='sm' leftIcon={<IoCheckmarkDone />} variant='ontime-ghosted-white'>
Make permanent
<Button onClick={applyDelayHandler} variant='ghosted-white'>
<IoCheckmarkDone /> Make permanent
</Button>
<Button onClick={cancelDelayHandler} size='sm' leftIcon={<IoClose />} variant='ontime-ghosted-white'>
<Button onClick={cancelDelayHandler} variant='ghosted-white'>
<IoClose />
Cancel
</Button>
</div>
@@ -30,7 +30,7 @@ export default function BlockRow({ blockId, colour, hidePast, rowId, rowIndex, t
}
return (
<tr className={style.blockRow} style={{ '--user-bg': colour }}>
<tr className={style.blockRow} style={{ '--user-bg': colour }} data-testid='cuesheet-block'>
{showActionMenu && (
<td className={style.actionColumn} tabIndex={-1} role='cell'>
<IconButton
@@ -18,6 +18,7 @@ function DelayRow({ duration, parentBgColour }: DelayRowProps) {
style={{
'--user-bg': parentBgColour ?? 'transparent',
}}
data-testid='cuesheet-delay'
>
<td tabIndex={0} role='cell'>
{delayTime}
@@ -84,6 +84,7 @@ export default function EventRow({
'--user-bg': parentBgColour ?? 'transparent',
}}
ref={selectedRef ?? ownRef}
data-testid='cuesheet-event'
>
{showActionMenu && (
<td className={style.actionColumn} tabIndex={-1} role='cell'>
@@ -5,7 +5,7 @@ import RotatedLink from '../../../../common/components/icons/RotatedLink';
import Modal from '../../../../common/components/modal/Modal';
import useInfo from '../../../../common/hooks-query/useInfo';
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
import GenerateLinkFormExport from '../../../../features/app-settings/panel/network-panel/GenerateLinkFormExport';
import GenerateLinkFormExport from '../../../../features/app-settings/panel/feature-panel/GenerateLinkFormExport';
function CuesheetShareModal() {
const { data: infoData } = useInfo();
+2 -7
View File
@@ -40,7 +40,7 @@ export default function Editor() {
if (isSettingsOpen) {
close();
} else {
setLocation('project');
setLocation('settings');
}
}, [close, isSettingsOpen, setLocation]);
@@ -54,12 +54,7 @@ export default function Editor() {
<IconButton aria-label='Toggle navigation' variant='subtle-white' size='xlarge' onClick={handler.open}>
<IoApps />
</IconButton>
<IconButton
aria-label='Toggle settings'
variant={isSettingsOpen ? 'subtle' : 'subtle-white'}
size='xlarge'
onClick={toggleSettings}
>
<IconButton aria-label='Toggle settings' variant='subtle-white' size='xlarge' onClick={toggleSettings}>
{isSettingsOpen ? <IoClose /> : <IoSettingsOutline />}
</IconButton>
</EditorOverview>
@@ -74,7 +74,7 @@ export default function Welcome({ onClose }: WelcomeProps) {
<div className={style.column}>
<div className={style.header}>
Welcome to Ontime
<IconButton variant='subtle-white'>
<IconButton aria-label='close welcome modal' variant='subtle-white'>
<IoClose />
</IconButton>
</div>
@@ -4,7 +4,6 @@
*/
import { ChangeEvent, useRef } from 'react';
import { Input } from '@chakra-ui/react';
import { uploadProjectFile } from '../../../../common/api/db';
import { invalidateAllCaches } from '../../../../common/api/utils';
@@ -41,7 +40,7 @@ export default function ImportProjectButton({ onFinish }: ImportProjectButtonPro
return (
<>
<Input
<input
ref={fileInputRef}
style={{ display: 'none' }}
type='file'
+6 -2
View File
@@ -9,8 +9,12 @@ const fileToDownload = 'e2e/tests/fixtures/tmp/';
test('project file upload', async ({ page }) => {
await page.goto('http://localhost:4001/editor');
if (await page.getByText('Welcome to Ontime')) {
await page.getByRole('button', { name: 'Close' }).click();
// Try to close welcome modal if it appears (times out silently if not present)
try {
await page.getByText('Welcome to Ontime').waitFor({ timeout: 1000 });
await page.getByRole('button', { name: 'close welcome modal' }).click();
} catch {
// Modal wasn't shown, continue with the test
}
await page.getByRole('button', { name: 'Edit' }).click();
+2 -2
View File
@@ -9,6 +9,6 @@ test('cuesheet displays events', async ({ page }) => {
await expect(page.locator('#cuesheet')).toBeVisible();
// there should be 16 rows in the table (same as the amount of events in the rundown)
const rowCount = await page.locator('#cuesheet tbody tr').count();
expect(rowCount).toBe(16);
await expect(page.getByTestId('cuesheet-event')).toHaveCount(14);
await expect(page.getByTestId('cuesheet-block')).toHaveCount(2);
});
+1 -3
View File
@@ -5,11 +5,9 @@ test('URL preset feature, it should redirect to given URL', async ({ page }) =>
// open settings
await page.getByRole('button', { name: 'Toggle settings' }).click();
await page.getByRole('button', { name: 'Feature Settings' }).click();
await page.getByRole('button', { name: 'URL Presets' }).click();
// create preset
await page.getByTestId('url-preset-form').scrollIntoViewIfNeeded();
await page.getByTestId('url-preset-form').getByRole('button', { name: 'New' }).scrollIntoViewIfNeeded();
await page.getByTestId('url-preset-form').getByRole('button', { name: 'New' }).click();