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