mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-21 23:19:09 +00:00
* fix: prevent add filter from submitting form * refactor: event clarifies whether automations exist but are disabled * refactor: improve readability of automation form * refactor: add affordance for field warnings * refactor: improve visibility of automation off state * refactor: apply warning styles to other feature toggles * Adding playback actions to automations (#2024) * adding intial automation actions * cleaning up * fixing formatting * ran oxfmt * switching action names to playback- to match the dropdown strings * fixing flicker that was caused by scroll arrows gettting unmounted --------- Co-authored-by: Cameron Slipp <cdslipp@gmail.com>
This commit is contained in:
@@ -125,6 +125,11 @@
|
|||||||
font-size: 0.5rem;
|
font-size: 0.5rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-content: center;
|
place-content: center;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
&[data-visible] {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function Select<T>({ options, fluid, size = 'medium', ...selectRo
|
|||||||
</BaseSelect.Trigger>
|
</BaseSelect.Trigger>
|
||||||
<BaseSelect.Portal>
|
<BaseSelect.Portal>
|
||||||
<BaseSelect.Positioner side='bottom' align='start'>
|
<BaseSelect.Positioner side='bottom' align='start'>
|
||||||
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} />
|
<BaseSelect.ScrollUpArrow className={styles.scrollArrow} keepMounted />
|
||||||
<BaseSelect.Popup className={styles.popup}>
|
<BaseSelect.Popup className={styles.popup}>
|
||||||
<BaseSelect.Arrow />
|
<BaseSelect.Arrow />
|
||||||
<BaseSelect.List className={styles.list}>
|
<BaseSelect.List className={styles.list}>
|
||||||
@@ -43,7 +43,7 @@ export default function Select<T>({ options, fluid, size = 'medium', ...selectRo
|
|||||||
))}
|
))}
|
||||||
</BaseSelect.List>
|
</BaseSelect.List>
|
||||||
</BaseSelect.Popup>
|
</BaseSelect.Popup>
|
||||||
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} />
|
<BaseSelect.ScrollDownArrow className={styles.scrollArrow} keepMounted />
|
||||||
</BaseSelect.Positioner>
|
</BaseSelect.Positioner>
|
||||||
</BaseSelect.Portal>
|
</BaseSelect.Portal>
|
||||||
</BaseSelect.Root>
|
</BaseSelect.Root>
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
.tag {
|
.tag {
|
||||||
font-size: calc(1rem - 3px);
|
font-size: calc(1rem - 3px);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
background-color: $gray-900;
|
|
||||||
color: $ui-white;
|
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0 0.25rem;
|
padding: 0 0.25rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.default {
|
||||||
|
background-color: $gray-900;
|
||||||
|
color: $ui-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
background-color: $orange-1300;
|
||||||
|
color: $orange-300;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { cx } from '../../utils/styleUtils';
|
||||||
|
|
||||||
import style from './Tag.module.scss';
|
import style from './Tag.module.scss';
|
||||||
|
|
||||||
interface TagProps {
|
interface TagProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
variant?: 'default' | 'warning';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Tag({ className, children }: PropsWithChildren<TagProps>) {
|
export default function Tag({ className, variant = 'default', children }: PropsWithChildren<TagProps>) {
|
||||||
return <span className={`${style.tag} ${className || ''}`}>{children}</span>;
|
return <span className={cx([style.tag, style[variant], className])}>{children}</span>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,11 +140,21 @@ $inner-padding: 1rem;
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fieldHeading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.fieldDescription {
|
.fieldDescription {
|
||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
color: $gray-400;
|
color: $gray-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warningText {
|
||||||
|
color: $orange-500;
|
||||||
|
}
|
||||||
|
|
||||||
.fieldError {
|
.fieldError {
|
||||||
font-size: calc(1rem - 2px);
|
font-size: calc(1rem - 2px);
|
||||||
color: $red-500;
|
color: $red-500;
|
||||||
|
|||||||
@@ -85,18 +85,34 @@ export function ListItem({ children }: { children: ReactNode }) {
|
|||||||
return <li className={style.listItem}>{children}</li>;
|
return <li className={style.listItem}>{children}</li>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Field({ title, description, error }: { title: string; description: string; error?: string }) {
|
export function Field({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
error,
|
||||||
|
descriptionTone = 'default',
|
||||||
|
}: {
|
||||||
|
title: ReactNode;
|
||||||
|
description: ReactNode;
|
||||||
|
error?: string;
|
||||||
|
descriptionTone?: 'default' | 'warning';
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={style.fieldTitle}>
|
<div className={style.fieldTitle}>
|
||||||
{title}
|
<div className={style.fieldHeading}>{title}</div>
|
||||||
{error && <Error>{error}</Error>}
|
{error && <Error>{error}</Error>}
|
||||||
{!error && description && <Description>{description}</Description>}
|
{!error && description && <Description tone={descriptionTone}>{description}</Description>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Description({ children }: { children: ReactNode }) {
|
export function Description({
|
||||||
return <div className={style.fieldDescription}>{children}</div>;
|
children,
|
||||||
|
tone = 'default',
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
tone?: 'default' | 'warning';
|
||||||
|
}) {
|
||||||
|
return <div className={cx([style.fieldDescription, tone === 'warning' && style.warningText])}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Highlight({ children }: { children: ReactNode }) {
|
export function Highlight({ children }: { children: ReactNode }) {
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ export default function AutomationForm({ automation, onClose }: AutomationFormPr
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div>
|
<div>
|
||||||
<Button type='submit' onClick={handleAddNewFilter}>
|
<Button onClick={handleAddNewFilter}>
|
||||||
Add filter <IoAdd />
|
Add filter <IoAdd />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export default function AutomationPanel({ location }: PanelBaseProps) {
|
|||||||
const automationsRef = useScrollIntoView<HTMLDivElement>('automations', location);
|
const automationsRef = useScrollIntoView<HTMLDivElement>('automations', location);
|
||||||
|
|
||||||
const isLoading = status === 'pending';
|
const isLoading = status === 'pending';
|
||||||
|
const automationState = isLoading ? undefined : data.enabledAutomations;
|
||||||
|
const oscInputState = isLoading ? undefined : data.enabledOscIn;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -24,13 +26,15 @@ export default function AutomationPanel({ location }: PanelBaseProps) {
|
|||||||
enabledAutomations={data.enabledAutomations}
|
enabledAutomations={data.enabledAutomations}
|
||||||
enabledOscIn={data.enabledOscIn}
|
enabledOscIn={data.enabledOscIn}
|
||||||
oscPortIn={data.oscPortIn}
|
oscPortIn={data.oscPortIn}
|
||||||
|
automationState={automationState}
|
||||||
|
oscInputState={oscInputState}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div ref={automationsRef}>
|
<div ref={automationsRef}>
|
||||||
<AutomationsList automations={data.automations} />
|
<AutomationsList automations={data.automations} enabledAutomations={automationState} />
|
||||||
</div>
|
</div>
|
||||||
<div ref={triggersRef}>
|
<div ref={triggersRef}>
|
||||||
<TriggersList triggers={data.triggers} automations={data.automations} />
|
<TriggersList triggers={data.triggers} automations={data.automations} enabledAutomations={automationState} />
|
||||||
</div>
|
</div>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+33
-7
@@ -7,6 +7,7 @@ import Info from '../../../../common/components/info/Info';
|
|||||||
import Input from '../../../../common/components/input/input/Input';
|
import Input from '../../../../common/components/input/input/Input';
|
||||||
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
||||||
import Switch from '../../../../common/components/switch/Switch';
|
import Switch from '../../../../common/components/switch/Switch';
|
||||||
|
import Tag from '../../../../common/components/tag/Tag';
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
||||||
import { isOntimeCloud } from '../../../../externals';
|
import { isOntimeCloud } from '../../../../externals';
|
||||||
@@ -18,12 +19,16 @@ interface AutomationSettingsProps {
|
|||||||
enabledAutomations: boolean;
|
enabledAutomations: boolean;
|
||||||
enabledOscIn: boolean;
|
enabledOscIn: boolean;
|
||||||
oscPortIn: number;
|
oscPortIn: number;
|
||||||
|
automationState?: boolean;
|
||||||
|
oscInputState?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationSettingsForm({
|
export default function AutomationSettingsForm({
|
||||||
enabledAutomations,
|
enabledAutomations,
|
||||||
enabledOscIn,
|
enabledOscIn,
|
||||||
oscPortIn,
|
oscPortIn,
|
||||||
|
automationState,
|
||||||
|
oscInputState,
|
||||||
}: AutomationSettingsProps) {
|
}: AutomationSettingsProps) {
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -56,6 +61,8 @@ export default function AutomationSettingsForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const canSubmit = !isSubmitting && isDirty && isValid;
|
const canSubmit = !isSubmitting && isDirty && isValid;
|
||||||
|
const automationsEnabled = watch('enabledAutomations');
|
||||||
|
const oscInputEnabled = watch('enabledOscIn');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel.Card>
|
<Panel.Card>
|
||||||
@@ -102,33 +109,52 @@ export default function AutomationSettingsForm({
|
|||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
title='Enable automations'
|
title={
|
||||||
description='Allow Ontime to send messages on lifecycle triggers'
|
<>
|
||||||
|
<span>Enable automations</span>
|
||||||
|
{automationState === false && <Tag variant='warning'>OFF</Tag>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
automationState === false
|
||||||
|
? 'Automations are OFF. Triggers stay configured, but Ontime will not send messages.'
|
||||||
|
: 'Allow Ontime to send messages on lifecycle triggers'
|
||||||
|
}
|
||||||
|
descriptionTone={automationState === false ? 'warning' : 'default'}
|
||||||
error={errors.enabledAutomations?.message}
|
error={errors.enabledAutomations?.message}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
size='large'
|
size='large'
|
||||||
checked={watch('enabledAutomations')}
|
checked={automationsEnabled}
|
||||||
onCheckedChange={(value: boolean) =>
|
onCheckedChange={(value: boolean) =>
|
||||||
setValue('enabledAutomations', value, { shouldDirty: true, shouldValidate: true })
|
setValue('enabledAutomations', value, { shouldDirty: true, shouldValidate: true })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Panel.ListItem>
|
</Panel.ListItem>
|
||||||
</Panel.ListGroup>
|
</Panel.ListGroup>
|
||||||
|
|
||||||
<Panel.Title>OSC Input</Panel.Title>
|
<Panel.Title>OSC Input</Panel.Title>
|
||||||
|
|
||||||
<Panel.ListGroup>
|
<Panel.ListGroup>
|
||||||
{isOntimeCloud && <Info>For security reasons OSC integrations are not available in the cloud service.</Info>}
|
{isOntimeCloud && <Info>For security reasons OSC integrations are not available in the cloud service.</Info>}
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
title='OSC input'
|
title={
|
||||||
description='Allow control of Ontime through OSC'
|
<>
|
||||||
|
<span>OSC input</span>
|
||||||
|
{oscInputState === false && <Tag variant='warning'>OFF</Tag>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
oscInputState === false
|
||||||
|
? 'OSC input is OFF. Ontime will not listen for incoming OSC control messages.'
|
||||||
|
: 'Allow control of Ontime through OSC'
|
||||||
|
}
|
||||||
|
descriptionTone={oscInputState === false ? 'warning' : 'default'}
|
||||||
error={errors.enabledOscIn?.message}
|
error={errors.enabledOscIn?.message}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
size='large'
|
size='large'
|
||||||
checked={watch('enabledOscIn')}
|
checked={oscInputEnabled}
|
||||||
onCheckedChange={(value: boolean) =>
|
onCheckedChange={(value: boolean) =>
|
||||||
setValue('enabledOscIn', value, { shouldDirty: true, shouldValidate: true })
|
setValue('enabledOscIn', value, { shouldDirty: true, shouldValidate: true })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { IoAdd, IoPencil, IoTrash } from 'react-icons/io5';
|
|||||||
import { deleteAutomation } from '../../../../common/api/automation';
|
import { deleteAutomation } from '../../../../common/api/automation';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
import Button from '../../../../common/components/buttons/Button';
|
||||||
|
import Info from '../../../../common/components/info/Info';
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||||
import Tag from '../../../../common/components/tag/Tag';
|
import Tag from '../../../../common/components/tag/Tag';
|
||||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||||
@@ -20,10 +21,11 @@ const automationPlaceholder: AutomationDTO = {
|
|||||||
|
|
||||||
interface AutomationsListProps {
|
interface AutomationsListProps {
|
||||||
automations: NormalisedAutomation;
|
automations: NormalisedAutomation;
|
||||||
|
enabledAutomations?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutomationsList(props: AutomationsListProps) {
|
export default function AutomationsList(props: AutomationsListProps) {
|
||||||
const { automations } = props;
|
const { automations, enabledAutomations } = props;
|
||||||
const { refetch } = useAutomationSettings();
|
const { refetch } = useAutomationSettings();
|
||||||
const [automationFormData, setAutomationFormData] = useState<AutomationDTO | null>(null);
|
const [automationFormData, setAutomationFormData] = useState<AutomationDTO | null>(null);
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
@@ -56,6 +58,13 @@ export default function AutomationsList(props: AutomationsListProps) {
|
|||||||
|
|
||||||
<Panel.Divider />
|
<Panel.Divider />
|
||||||
|
|
||||||
|
{enabledAutomations === false && (
|
||||||
|
<Info>
|
||||||
|
Automations are disabled. You can still manage automation definitions here, but they will not run until
|
||||||
|
enabled.
|
||||||
|
</Info>
|
||||||
|
)}
|
||||||
|
|
||||||
{automationFormData !== null && (
|
{automationFormData !== null && (
|
||||||
<AutomationForm automation={automationFormData} onClose={() => setAutomationFormData(null)} />
|
<AutomationForm automation={automationFormData} onClose={() => setAutomationFormData(null)} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ export default function OntimeActionForm({
|
|||||||
{ value: 'aux2-set', label: 'Aux 2: set' },
|
{ value: 'aux2-set', label: 'Aux 2: set' },
|
||||||
{ value: 'aux3-set', label: 'Aux 3: set' },
|
{ value: 'aux3-set', label: 'Aux 3: set' },
|
||||||
|
|
||||||
|
{ value: 'playback-start', label: 'Playback: start' },
|
||||||
|
{ value: 'playback-stop', label: 'Playback: stop' },
|
||||||
|
{ value: 'playback-pause', label: 'Playback: pause' },
|
||||||
|
{ value: 'playback-roll', label: 'Playback: roll' },
|
||||||
|
|
||||||
{ value: 'message-set', label: 'Primary Message: set' },
|
{ value: 'message-set', label: 'Primary Message: set' },
|
||||||
{ value: 'message-secondary', label: 'Secondary Message: source' },
|
{ value: 'message-secondary', label: 'Secondary Message: source' },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { IoAdd } from 'react-icons/io5';
|
|||||||
import { deleteTrigger } from '../../../../common/api/automation';
|
import { deleteTrigger } from '../../../../common/api/automation';
|
||||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
import Button from '../../../../common/components/buttons/Button';
|
||||||
|
import Info from '../../../../common/components/info/Info';
|
||||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
import { checkDuplicates } from './automationUtils';
|
import { checkDuplicates } from './automationUtils';
|
||||||
@@ -14,10 +15,11 @@ import TriggersListItem from './TriggersListItem';
|
|||||||
interface TriggersListProps {
|
interface TriggersListProps {
|
||||||
triggers: Trigger[];
|
triggers: Trigger[];
|
||||||
automations: NormalisedAutomation;
|
automations: NormalisedAutomation;
|
||||||
|
enabledAutomations?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TriggersList(props: TriggersListProps) {
|
export default function TriggersList(props: TriggersListProps) {
|
||||||
const { triggers, automations } = props;
|
const { triggers, automations, enabledAutomations } = props;
|
||||||
const [showForm, setShowForm] = useState(false);
|
const [showForm, setShowForm] = useState(false);
|
||||||
const { refetch } = useAutomationSettings();
|
const { refetch } = useAutomationSettings();
|
||||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
@@ -52,6 +54,9 @@ export default function TriggersList(props: TriggersListProps) {
|
|||||||
</Panel.SubHeader>
|
</Panel.SubHeader>
|
||||||
<Panel.Divider />
|
<Panel.Divider />
|
||||||
<Panel.Section>
|
<Panel.Section>
|
||||||
|
{enabledAutomations === false && (
|
||||||
|
<Info>Automations are disabled. You can still manage triggers here, but they will not run until enabled.</Info>
|
||||||
|
)}
|
||||||
{duplicates && (
|
{duplicates && (
|
||||||
<Panel.Error>
|
<Panel.Error>
|
||||||
You have created multiple links between the same trigger and automation which can performance issues.
|
You have created multiple links between the same trigger and automation which can performance issues.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Info from '../../../../common/components/info/Info';
|
|||||||
import { SwatchPickerRHF } from '../../../../common/components/input/colour-input/SwatchPicker';
|
import { SwatchPickerRHF } from '../../../../common/components/input/colour-input/SwatchPicker';
|
||||||
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/link/external-link/ExternalLink';
|
||||||
import Switch from '../../../../common/components/switch/Switch';
|
import Switch from '../../../../common/components/switch/Switch';
|
||||||
|
import Tag from '../../../../common/components/tag/Tag';
|
||||||
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
|
||||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||||
import * as Panel from '../../panel-utils/PanelUtils';
|
import * as Panel from '../../panel-utils/PanelUtils';
|
||||||
@@ -55,6 +56,8 @@ export default function ViewSettings() {
|
|||||||
reset(data);
|
reset(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const overrideStylesEnabled = watch('overrideStyles');
|
||||||
|
|
||||||
if (!control) {
|
if (!control) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -91,12 +94,22 @@ export default function ViewSettings() {
|
|||||||
<CodeEditorModal isOpen={isCodeEditorOpen} onClose={codeEditorHandler.close} />
|
<CodeEditorModal isOpen={isCodeEditorOpen} onClose={codeEditorHandler.close} />
|
||||||
<Panel.ListItem>
|
<Panel.ListItem>
|
||||||
<Panel.Field
|
<Panel.Field
|
||||||
title='Override CSS styles'
|
title={
|
||||||
description='Enables overriding view styles with custom stylesheet'
|
<>
|
||||||
|
<span>Override CSS styles</span>
|
||||||
|
{overrideStylesEnabled && <Tag variant='warning'>ON</Tag>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
overrideStylesEnabled
|
||||||
|
? 'CSS override is ON. Ontime views will use the custom override stylesheet.'
|
||||||
|
: 'Enables overriding view styles with custom stylesheet'
|
||||||
|
}
|
||||||
|
descriptionTone={overrideStylesEnabled ? 'warning' : 'default'}
|
||||||
/>
|
/>
|
||||||
<Switch
|
<Switch
|
||||||
size='large'
|
size='large'
|
||||||
checked={watch('overrideStyles')}
|
checked={overrideStylesEnabled}
|
||||||
onCheckedChange={(value: boolean) => setValue('overrideStyles', value, { shouldDirty: true })}
|
onCheckedChange={(value: boolean) => setValue('overrideStyles', value, { shouldDirty: true })}
|
||||||
/>
|
/>
|
||||||
<Button onClick={codeEditorHandler.open} disabled={isSubmitting}>
|
<Button onClick={codeEditorHandler.open} disabled={isSubmitting}>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { TbFlagFilled } from 'react-icons/tb';
|
|||||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||||
|
|
||||||
import { useEntryActionsContext } from '../../common/context/EntryActionsContext';
|
import { useEntryActionsContext } from '../../common/context/EntryActionsContext';
|
||||||
|
import useAutomationSettings from '../../common/hooks-query/useAutomationSettings';
|
||||||
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
import { useEntryCopy } from '../../common/stores/entryCopyStore';
|
||||||
import { RundownMetadataObject, lastMetadataKey } from '../../common/utils/rundownMetadata';
|
import { RundownMetadataObject, lastMetadataKey } from '../../common/utils/rundownMetadata';
|
||||||
import { AppMode } from '../../ontimeConfig';
|
import { AppMode } from '../../ontimeConfig';
|
||||||
@@ -49,6 +50,8 @@ export default function Rundown({ order, flatOrder, entries, id, rundownMetadata
|
|||||||
// invoke the compiler for the component
|
// invoke the compiler for the component
|
||||||
'use memo';
|
'use memo';
|
||||||
|
|
||||||
|
const { data: automationSettings, status: automationStatus } = useAutomationSettings();
|
||||||
|
const automationsEnabled = automationStatus === 'pending' ? undefined : automationSettings.enabledAutomations;
|
||||||
const [sortableData, setSortableData] = useState<EntryId[]>(() => makeSortableList(order, entries));
|
const [sortableData, setSortableData] = useState<EntryId[]>(() => makeSortableList(order, entries));
|
||||||
const [metadata, setMetadata] = useState<RundownMetadataObject>(rundownMetadata);
|
const [metadata, setMetadata] = useState<RundownMetadataObject>(rundownMetadata);
|
||||||
|
|
||||||
@@ -263,6 +266,7 @@ export default function Rundown({ order, flatOrder, entries, id, rundownMetadata
|
|||||||
isPast={entryMetadata.isPast}
|
isPast={entryMetadata.isPast}
|
||||||
eventIndex={entryMetadata.eventIndex}
|
eventIndex={entryMetadata.eventIndex}
|
||||||
data={entry}
|
data={entry}
|
||||||
|
automationsEnabled={automationsEnabled}
|
||||||
loaded={entryMetadata.isLoaded}
|
loaded={entryMetadata.isLoaded}
|
||||||
hasCursor={hasCursor}
|
hasCursor={hasCursor}
|
||||||
isNext={isNext}
|
isNext={isNext}
|
||||||
@@ -282,7 +286,18 @@ export default function Rundown({ order, flatOrder, entries, id, rundownMetadata
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[entries, metadata, getIsCollapsed, isEditMode, cursor, nextEventId, playback, lastEntryId, handleCollapseGroup],
|
[
|
||||||
|
entries,
|
||||||
|
metadata,
|
||||||
|
getIsCollapsed,
|
||||||
|
isEditMode,
|
||||||
|
cursor,
|
||||||
|
nextEventId,
|
||||||
|
playback,
|
||||||
|
lastEntryId,
|
||||||
|
handleCollapseGroup,
|
||||||
|
automationsEnabled,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sortableData.length < 1) {
|
if (sortableData.length < 1) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface RundownEntryProps {
|
|||||||
type: SupportedEntry;
|
type: SupportedEntry;
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
data: OntimeEntry;
|
data: OntimeEntry;
|
||||||
|
automationsEnabled?: boolean;
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
eventIndex: number;
|
eventIndex: number;
|
||||||
hasCursor: boolean;
|
hasCursor: boolean;
|
||||||
@@ -22,6 +23,7 @@ interface RundownEntryProps {
|
|||||||
export default function RundownEntry({
|
export default function RundownEntry({
|
||||||
isPast,
|
isPast,
|
||||||
data,
|
data,
|
||||||
|
automationsEnabled,
|
||||||
loaded,
|
loaded,
|
||||||
hasCursor,
|
hasCursor,
|
||||||
isNext,
|
isNext,
|
||||||
@@ -52,6 +54,7 @@ export default function RundownEntry({
|
|||||||
title={data.title}
|
title={data.title}
|
||||||
note={data.note}
|
note={data.note}
|
||||||
delay={data.delay}
|
delay={data.delay}
|
||||||
|
automationsEnabled={automationsEnabled}
|
||||||
colour={data.colour}
|
colour={data.colour}
|
||||||
isPast={isPast}
|
isPast={isPast}
|
||||||
isNext={isNext}
|
isNext={isNext}
|
||||||
|
|||||||
+73
-13
@@ -1,30 +1,90 @@
|
|||||||
.triggerForm {
|
.triggers {
|
||||||
padding-block: 0.5rem;
|
display: flex;
|
||||||
display: grid;
|
flex-direction: column;
|
||||||
grid-template-columns: 8rem 1fr auto 2rem;
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section,
|
||||||
|
.formSection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: $aux-text-size;
|
||||||
|
color: $label-gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: $black-10;
|
||||||
|
border: 1px solid $white-10;
|
||||||
|
border-radius: $component-border-radius-md;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerForm {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formFields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 8rem 1fr;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formActions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trigger {
|
.trigger {
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.5rem 0.75rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 8rem 1fr 2rem;
|
grid-template-columns: 8rem 1fr 2rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
|
||||||
&:nth-child(even) {
|
&:not(:first-child) {
|
||||||
background-color: $white-1;
|
border-top: 1px solid $white-10;
|
||||||
}
|
|
||||||
|
|
||||||
& > span {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorLabel {
|
.triggerMeta {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metaLabel {
|
||||||
|
color: $label-gray;
|
||||||
|
font-size: $aux-text-size;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automationTitle {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validationError,
|
||||||
|
.validationSuccess {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
font-size: $aux-text-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validationError {
|
||||||
color: $red-500;
|
color: $red-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success {
|
.validationSuccess {
|
||||||
color: $green-500;
|
color: $green-500;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { TimerLifeCycle, Trigger, timerLifecycleValues } from 'ontime-types';
|
import { NormalisedAutomation, TimerLifeCycle, Trigger, timerLifecycleValues } from 'ontime-types';
|
||||||
import { generateId } from 'ontime-utils';
|
import { generateId } from 'ontime-utils';
|
||||||
import { Fragment, useCallback, useMemo, useState } from 'react';
|
import { Fragment, useCallback, useMemo, useState } from 'react';
|
||||||
import { IoAlertCircle, IoCheckmarkCircle, IoTrash } from 'react-icons/io5';
|
import { IoAlertCircle, IoCheckmarkCircle, IoTrash } from 'react-icons/io5';
|
||||||
|
|
||||||
import Button from '../../../../common/components/buttons/Button';
|
import Button from '../../../../common/components/buttons/Button';
|
||||||
|
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||||
|
import Info from '../../../../common/components/info/Info';
|
||||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||||
import Select from '../../../../common/components/select/Select';
|
import Select from '../../../../common/components/select/Select';
|
||||||
import Tag from '../../../../common/components/tag/Tag';
|
|
||||||
import Tooltip from '../../../../common/components/tooltip/Tooltip';
|
|
||||||
import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext';
|
import { useEntryActionsContext } from '../../../../common/context/EntryActionsContext';
|
||||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||||
import { eventTriggerOptions } from './eventTrigger.constants';
|
import { eventTriggerOptions } from './eventTrigger.constants';
|
||||||
@@ -20,23 +20,36 @@ interface EventEditorTriggersProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function EventEditorTriggers({ triggers, eventId }: EventEditorTriggersProps) {
|
export default function EventEditorTriggers({ triggers, eventId }: EventEditorTriggersProps) {
|
||||||
|
const { data: automationSettings, status: automationStatus } = useAutomationSettings();
|
||||||
|
const automationsEnabled = automationStatus === 'pending' ? undefined : automationSettings.enabledAutomations;
|
||||||
const showTriggers = triggers.length > 0;
|
const showTriggers = triggers.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={style.triggers}>
|
||||||
{showTriggers && <ExistingEventTriggers triggers={triggers} eventId={eventId} />}
|
{automationsEnabled === false && (
|
||||||
<EventTriggerForm triggers={triggers} eventId={eventId} />
|
<Info>Automations are disabled. Event triggers stay configured, but they will not run until enabled.</Info>
|
||||||
</>
|
)}
|
||||||
|
{showTriggers && (
|
||||||
|
<div className={style.section}>
|
||||||
|
<div className={style.sectionTitle}>Applied automations</div>
|
||||||
|
<ExistingEventTriggers triggers={triggers} eventId={eventId} automations={automationSettings.automations} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Editor.Panel className={style.formSection}>
|
||||||
|
<div className={style.sectionTitle}>Add automation</div>
|
||||||
|
<EventTriggerForm triggers={triggers} eventId={eventId} automations={automationSettings.automations} />
|
||||||
|
</Editor.Panel>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventTriggerFormProps {
|
interface EventTriggerFormProps {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
triggers?: Trigger[];
|
triggers?: Trigger[];
|
||||||
|
automations: NormalisedAutomation;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EventTriggerForm({ eventId, triggers }: EventTriggerFormProps) {
|
function EventTriggerForm({ eventId, triggers, automations }: EventTriggerFormProps) {
|
||||||
const { data: automationSettings } = useAutomationSettings();
|
|
||||||
const { updateEntry } = useEntryActionsContext();
|
const { updateEntry } = useEntryActionsContext();
|
||||||
const [automationId, setAutomationId] = useState<string | undefined>(undefined);
|
const [automationId, setAutomationId] = useState<string | undefined>(undefined);
|
||||||
const [cycleValue, setCycleValue] = useState(TimerLifeCycle.onStart);
|
const [cycleValue, setCycleValue] = useState(TimerLifeCycle.onStart);
|
||||||
@@ -52,7 +65,7 @@ function EventTriggerForm({ eventId, triggers }: EventTriggerFormProps) {
|
|||||||
if (automationId === undefined) {
|
if (automationId === undefined) {
|
||||||
return 'Select an automation';
|
return 'Select an automation';
|
||||||
}
|
}
|
||||||
if (!Object.keys(automationSettings.automations).includes(automationId)) {
|
if (!Object.keys(automations).includes(automationId)) {
|
||||||
return 'This automation does not exist';
|
return 'This automation does not exist';
|
||||||
}
|
}
|
||||||
if (triggers === undefined) {
|
if (triggers === undefined) {
|
||||||
@@ -64,10 +77,11 @@ function EventTriggerForm({ eventId, triggers }: EventTriggerFormProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const validationError = getValidationError(cycleValue, automationId);
|
const validationError = getValidationError(cycleValue, automationId);
|
||||||
|
const validationLabel = validationError ?? 'Ready to add automation';
|
||||||
|
|
||||||
const triggerOptions = useMemo(
|
const triggerOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ value: null, label: 'Select Trigger' },
|
{ value: null, label: 'Select lifecycle' },
|
||||||
...eventTriggerOptions.map((cycle) => ({ value: cycle, label: cycle })),
|
...eventTriggerOptions.map((cycle) => ({ value: cycle, label: cycle })),
|
||||||
],
|
],
|
||||||
[], // eventTriggerOptions is a constant, no need for dependency
|
[], // eventTriggerOptions is a constant, no need for dependency
|
||||||
@@ -76,42 +90,49 @@ function EventTriggerForm({ eventId, triggers }: EventTriggerFormProps) {
|
|||||||
const automationOptions = useMemo(
|
const automationOptions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ value: null, label: 'Select Automation' },
|
{ value: null, label: 'Select Automation' },
|
||||||
...Object.values(automationSettings.automations).map(({ id, title }) => ({ value: id, label: title })),
|
...Object.values(automations).map(({ id, title }) => ({ value: id, label: title })),
|
||||||
],
|
],
|
||||||
[automationSettings.automations], // This needs to be a dependency as it can change
|
[automations], // This needs to be a dependency as it can change
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.triggerForm}>
|
<div className={style.triggerForm}>
|
||||||
<Select
|
<div className={style.formFields}>
|
||||||
value={cycleValue}
|
<div>
|
||||||
onValueChange={(value) => {
|
<Editor.Label>Lifecycle</Editor.Label>
|
||||||
if (value !== null) setCycleValue(value);
|
<Select
|
||||||
}}
|
value={cycleValue}
|
||||||
options={triggerOptions}
|
onValueChange={(value) => {
|
||||||
/>
|
if (value !== null) setCycleValue(value);
|
||||||
|
}}
|
||||||
|
options={triggerOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Select
|
<div>
|
||||||
value={automationId ?? null}
|
<Editor.Label>Automation</Editor.Label>
|
||||||
onValueChange={(value) => {
|
<Select
|
||||||
if (value !== null) setAutomationId(value);
|
value={automationId ?? null}
|
||||||
}}
|
onValueChange={(value) => {
|
||||||
options={automationOptions}
|
if (value !== null) setAutomationId(value);
|
||||||
/>
|
}}
|
||||||
|
options={automationOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<div className={style.formActions}>
|
||||||
disabled={validationError !== undefined}
|
<div className={validationError ? style.validationError : style.validationSuccess}>
|
||||||
onClick={() => automationId && handleSubmit(cycleValue, automationId)}
|
{validationError ? <IoAlertCircle /> : <IoCheckmarkCircle />}
|
||||||
>
|
<span>{validationLabel}</span>
|
||||||
Add
|
</div>
|
||||||
</Button>
|
<Button
|
||||||
{validationError !== undefined ? (
|
disabled={validationError !== undefined}
|
||||||
<Tooltip text={validationError} render={<span />}>
|
onClick={() => automationId && handleSubmit(cycleValue, automationId)}
|
||||||
<IoAlertCircle className={style.errorLabel} />
|
>
|
||||||
</Tooltip>
|
Add automation
|
||||||
) : (
|
</Button>
|
||||||
<IoCheckmarkCircle className={style.success} />
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -119,11 +140,11 @@ function EventTriggerForm({ eventId, triggers }: EventTriggerFormProps) {
|
|||||||
interface ExistingEventTriggersProps {
|
interface ExistingEventTriggersProps {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
triggers: Trigger[];
|
triggers: Trigger[];
|
||||||
|
automations: NormalisedAutomation;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExistingEventTriggers({ eventId, triggers }: ExistingEventTriggersProps) {
|
function ExistingEventTriggers({ eventId, triggers, automations }: ExistingEventTriggersProps) {
|
||||||
const { updateEntry } = useEntryActionsContext();
|
const { updateEntry } = useEntryActionsContext();
|
||||||
const { data: automationSettings } = useAutomationSettings();
|
|
||||||
|
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
(triggerId: string) => {
|
(triggerId: string) => {
|
||||||
@@ -144,16 +165,22 @@ function ExistingEventTriggers({ eventId, triggers }: ExistingEventTriggersProps
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={style.triggerList}>
|
||||||
{Object.entries(filteredTriggers).map(([triggerLifeCycle, triggerGroup]) => (
|
{Object.entries(filteredTriggers).map(([triggerLifeCycle, triggerGroup]) => (
|
||||||
<Fragment key={triggerLifeCycle}>
|
<Fragment key={triggerLifeCycle}>
|
||||||
{triggerGroup.map((trigger) => {
|
{triggerGroup.map((trigger) => {
|
||||||
const { id, automationId } = trigger;
|
const { id, automationId } = trigger;
|
||||||
const automationTitle = automationSettings.automations[automationId]?.title ?? '<MISSING AUTOMATION>';
|
const automationTitle = automations[automationId]?.title ?? '<MISSING AUTOMATION>';
|
||||||
return (
|
return (
|
||||||
<div key={id} className={style.trigger}>
|
<div key={id} className={style.trigger}>
|
||||||
<Tag>{triggerLifeCycle}</Tag>
|
<div className={style.triggerMeta}>
|
||||||
<Tag>{automationTitle}</Tag>
|
<div className={style.metaLabel}>Lifecycle</div>
|
||||||
|
<div>{triggerLifeCycle}</div>
|
||||||
|
</div>
|
||||||
|
<div className={style.triggerMeta}>
|
||||||
|
<div className={style.metaLabel}>Automation</div>
|
||||||
|
<div className={style.automationTitle}>{automationTitle}</div>
|
||||||
|
</div>
|
||||||
<IconButton variant='ghosted-destructive' onClick={() => handleDelete(id)}>
|
<IconButton variant='ghosted-destructive' onClick={() => handleDelete(id)}>
|
||||||
<IoTrash />
|
<IoTrash />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -231,6 +231,10 @@ $skip-opacity: 0.2;
|
|||||||
color: var(--status-color-active-override, $active-indicator);
|
color: var(--status-color-active-override, $active-indicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statusIcon.warning {
|
||||||
|
color: $orange-500;
|
||||||
|
}
|
||||||
|
|
||||||
.statusIcon.disabled {
|
.statusIcon.disabled {
|
||||||
color: $gray-1000;
|
color: $gray-1000;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ interface RundownEventProps {
|
|||||||
title: string;
|
title: string;
|
||||||
note: string;
|
note: string;
|
||||||
delay: number;
|
delay: number;
|
||||||
|
automationsEnabled?: boolean;
|
||||||
colour: string;
|
colour: string;
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
isNext: boolean;
|
isNext: boolean;
|
||||||
@@ -76,6 +77,7 @@ export default function RundownEvent({
|
|||||||
title,
|
title,
|
||||||
note,
|
note,
|
||||||
delay,
|
delay,
|
||||||
|
automationsEnabled,
|
||||||
colour,
|
colour,
|
||||||
isPast,
|
isPast,
|
||||||
isNext,
|
isNext,
|
||||||
@@ -302,6 +304,7 @@ export default function RundownEvent({
|
|||||||
title={title}
|
title={title}
|
||||||
note={note}
|
note={note}
|
||||||
delay={delay}
|
delay={delay}
|
||||||
|
automationsEnabled={automationsEnabled}
|
||||||
isNext={isNext}
|
isNext={isNext}
|
||||||
skip={skip}
|
skip={skip}
|
||||||
loaded={loaded}
|
loaded={loaded}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ interface RundownEventInnerProps {
|
|||||||
title: string;
|
title: string;
|
||||||
note: string;
|
note: string;
|
||||||
delay: number;
|
delay: number;
|
||||||
|
automationsEnabled?: boolean;
|
||||||
isNext: boolean;
|
isNext: boolean;
|
||||||
skip: boolean;
|
skip: boolean;
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
@@ -64,6 +65,7 @@ function RundownEventInner({
|
|||||||
title,
|
title,
|
||||||
note,
|
note,
|
||||||
delay,
|
delay,
|
||||||
|
automationsEnabled,
|
||||||
isNext,
|
isNext,
|
||||||
skip = false,
|
skip = false,
|
||||||
loaded,
|
loaded,
|
||||||
@@ -79,6 +81,21 @@ function RundownEventInner({
|
|||||||
|
|
||||||
const eventIsPlaying = playback === Playback.Play;
|
const eventIsPlaying = playback === Playback.Play;
|
||||||
const eventIsPaused = playback === Playback.Pause;
|
const eventIsPaused = playback === Playback.Pause;
|
||||||
|
const automationTooltip = (() => {
|
||||||
|
if (!hasTriggers) {
|
||||||
|
return 'Event has no triggers';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (automationsEnabled !== false) {
|
||||||
|
return 'Event has triggers';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Event has triggers, but automations are disabled';
|
||||||
|
})();
|
||||||
|
const automationIconClasses = cx([
|
||||||
|
style.statusIcon,
|
||||||
|
hasTriggers ? (automationsEnabled === false ? style.warning : style.active) : style.disabled,
|
||||||
|
]);
|
||||||
|
|
||||||
const playBtnStyles = { _hover: {} };
|
const playBtnStyles = { _hover: {} };
|
||||||
if (!skip && eventIsPlaying) {
|
if (!skip && eventIsPlaying) {
|
||||||
@@ -142,8 +159,8 @@ function RundownEventInner({
|
|||||||
<Tooltip text={`${countToEnd ? 'Count to End' : 'Count duration'}`} render={<span />}>
|
<Tooltip text={`${countToEnd ? 'Count to End' : 'Count duration'}`} render={<span />}>
|
||||||
<LuArrowDownToLine className={`${style.statusIcon} ${countToEnd ? style.active : style.disabled}`} />
|
<LuArrowDownToLine className={`${style.statusIcon} ${countToEnd ? style.active : style.disabled}`} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip text='Event has Triggers' render={<span />}>
|
<Tooltip text={automationTooltip} render={<span />}>
|
||||||
<IoFlash className={`${style.statusIcon} ${hasTriggers ? style.active : style.disabled}`} />
|
<IoFlash className={automationIconClasses} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { parseUserTime } from 'ontime-utils';
|
|||||||
import { logger } from '../../../classes/Logger.js';
|
import { logger } from '../../../classes/Logger.js';
|
||||||
import { auxTimerService } from '../../../services/aux-timer-service/AuxTimerService.js';
|
import { auxTimerService } from '../../../services/aux-timer-service/AuxTimerService.js';
|
||||||
import * as messageService from '../../../services/message-service/message.service.js';
|
import * as messageService from '../../../services/message-service/message.service.js';
|
||||||
|
import { runtimeService } from '../../../services/runtime-service/runtime.service.js';
|
||||||
|
|
||||||
export function toOntimeAction(action: OntimeAction) {
|
export function toOntimeAction(action: OntimeAction) {
|
||||||
const actionType = action.action;
|
const actionType = action.action;
|
||||||
@@ -40,6 +41,16 @@ export function toOntimeAction(action: OntimeAction) {
|
|||||||
return auxTimerService.setTime(time, 3);
|
return auxTimerService.setTime(time, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Playback actions
|
||||||
|
case 'playback-start':
|
||||||
|
return runtimeService.start();
|
||||||
|
case 'playback-stop':
|
||||||
|
return runtimeService.stop();
|
||||||
|
case 'playback-pause':
|
||||||
|
return runtimeService.pause();
|
||||||
|
case 'playback-roll':
|
||||||
|
return runtimeService.roll();
|
||||||
|
|
||||||
// Message actions
|
// Message actions
|
||||||
case 'message-set': {
|
case 'message-set': {
|
||||||
messageService.patch({
|
messageService.patch({
|
||||||
|
|||||||
@@ -68,15 +68,24 @@ const ontimeAuxTriggerAction = [
|
|||||||
|
|
||||||
const ontimeAuxSetAction = ['aux1-set', 'aux2-set', 'aux3-set'] as const;
|
const ontimeAuxSetAction = ['aux1-set', 'aux2-set', 'aux3-set'] as const;
|
||||||
|
|
||||||
|
const ontimePlaybackAction = ['playback-start', 'playback-stop', 'playback-pause', 'playback-roll'] as const;
|
||||||
|
|
||||||
type OntimeAuxTriggerAction = (typeof ontimeAuxTriggerAction)[number];
|
type OntimeAuxTriggerAction = (typeof ontimeAuxTriggerAction)[number];
|
||||||
type OntimeAuxSetAction = (typeof ontimeAuxSetAction)[number];
|
type OntimeAuxSetAction = (typeof ontimeAuxSetAction)[number];
|
||||||
|
type OntimePlaybackAction = (typeof ontimePlaybackAction)[number];
|
||||||
type OntimeMessageSet = 'message-set';
|
type OntimeMessageSet = 'message-set';
|
||||||
type OntimeMessageSecondary = 'message-secondary';
|
type OntimeMessageSecondary = 'message-secondary';
|
||||||
|
|
||||||
export type OntimeActionKey = OntimeAuxTriggerAction | OntimeAuxSetAction | OntimeMessageSet | OntimeMessageSecondary;
|
export type OntimeActionKey =
|
||||||
|
| OntimeAuxTriggerAction
|
||||||
|
| OntimePlaybackAction
|
||||||
|
| OntimeAuxSetAction
|
||||||
|
| OntimeMessageSet
|
||||||
|
| OntimeMessageSecondary;
|
||||||
|
|
||||||
export const ontimeActionKeyValues = [
|
export const ontimeActionKeyValues = [
|
||||||
...ontimeAuxTriggerAction,
|
...ontimeAuxTriggerAction,
|
||||||
|
...ontimePlaybackAction,
|
||||||
...ontimeAuxSetAction,
|
...ontimeAuxSetAction,
|
||||||
'message-set',
|
'message-set',
|
||||||
'message-secondary',
|
'message-secondary',
|
||||||
@@ -87,6 +96,10 @@ export type OntimeAction =
|
|||||||
type: 'ontime';
|
type: 'ontime';
|
||||||
action: OntimeAuxTriggerAction;
|
action: OntimeAuxTriggerAction;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'ontime';
|
||||||
|
action: OntimePlaybackAction;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'ontime';
|
type: 'ontime';
|
||||||
action: OntimeAuxSetAction;
|
action: OntimeAuxSetAction;
|
||||||
|
|||||||
Reference in New Issue
Block a user