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