mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 11:23:50 +00:00
refactor: migrate UI components and remove chakra
migrate pin page migrate copy tag migrate pin input migrate dropdown menus migrate radio buttons migrate switch migrate select migrate textarea migrate colour picker migrate select migrate context menu migrate viewparams remove chakra
This commit is contained in:
committed by
Carlos Valente
parent
4d359445a7
commit
e1e8410ba4
@@ -150,9 +150,9 @@ $inner-padding: 1rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border: none;
|
||||
border-top: 1px solid $white-10;
|
||||
margin: 1rem -2rem;
|
||||
z-index: $zindex-floating;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
|
||||
-5
@@ -13,11 +13,6 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.matchRadio {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.ruleSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { IoAdd, IoTrash } from 'react-icons/io5';
|
||||
import { Radio, RadioGroup, Select } from '@chakra-ui/react';
|
||||
import {
|
||||
Automation,
|
||||
AutomationDTO,
|
||||
@@ -20,6 +19,8 @@ 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 RadioGroup from '../../../../common/components/radio-group/RadioGroup';
|
||||
import Select from '../../../../common/components/select/Select';
|
||||
import Tag from '../../../../common/components/tag/Tag';
|
||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||
import useCustomFields from '../../../../common/hooks-query/useCustomFields';
|
||||
@@ -214,15 +215,14 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
<div className={style.ruleSection}>
|
||||
<label>
|
||||
Trigger outputs if
|
||||
<Controller
|
||||
name='filterRule'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup {...field} size='sm' className={style.matchRadio} variant='ontime'>
|
||||
<Radio value='all'>All filters pass</Radio>
|
||||
<Radio value='any'>Any filter passes</Radio>
|
||||
</RadioGroup>
|
||||
)}
|
||||
<RadioGroup
|
||||
orientation='horizontal'
|
||||
value={watch('filterRule')}
|
||||
onValueChange={(value) => setValue('filterRule', value, { shouldDirty: true })}
|
||||
items={[
|
||||
{ value: 'all', label: 'All filters pass' },
|
||||
{ value: 'any', label: 'Any filter passes' },
|
||||
]}
|
||||
/>
|
||||
</label>
|
||||
{fieldFilters.map((field, index) => {
|
||||
@@ -232,43 +232,29 @@ export default function AutomationForm(props: AutomationFormProps) {
|
||||
<label>
|
||||
Runtime data source
|
||||
<Select
|
||||
{...register(`filters.${index}.field`, { required: { value: true, message: 'Required field' } })}
|
||||
size='sm'
|
||||
variant='ontime'
|
||||
>
|
||||
<option selected hidden disabled value=''>
|
||||
Event field
|
||||
</option>
|
||||
{fieldList.map(({ value, label }, localIndex) => {
|
||||
const key = `filters.${index}.field.${localIndex}`;
|
||||
return (
|
||||
<option key={key} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
value={watch(`filters.${index}.field`)}
|
||||
onValueChange={(value) => setValue(`filters.${index}.field`, value, { shouldDirty: true })}
|
||||
options={fieldList.map(({ value, label }) => ({ value, label }))}
|
||||
aria-label='Event field'
|
||||
/>
|
||||
<Panel.Error>{errors.filters?.[index]?.field?.message}</Panel.Error>
|
||||
</label>
|
||||
<label>
|
||||
Matching condition
|
||||
<Select
|
||||
{...register(`filters.${index}.operator`, { required: { value: true, message: 'Required field' } })}
|
||||
size='sm'
|
||||
variant='ontime'
|
||||
>
|
||||
<option selected hidden disabled value=''>
|
||||
Operator
|
||||
</option>
|
||||
<option value='equals'>equals</option>
|
||||
<option value='not_equals'>not equals</option>
|
||||
<option value='contains'>contains</option>
|
||||
{/*
|
||||
We dont currently offer a data source where these operators would make sense
|
||||
<option value='greater_than'>greater than</option>
|
||||
<option value='less_than'>less than</option>
|
||||
*/}
|
||||
</Select>
|
||||
value={watch(`filters.${index}.operator`)}
|
||||
onValueChange={(value) =>
|
||||
setValue(`filters.${index}.operator`, value as 'equals' | 'not_equals' | 'contains', {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
options={[
|
||||
{ value: 'equals', label: 'equals' },
|
||||
{ value: 'not_equals', label: 'not equals' },
|
||||
{ value: 'contains', label: 'contains' },
|
||||
]}
|
||||
aria-label='Operator'
|
||||
/>
|
||||
<Panel.Error>{errors.filters?.[index]?.operator?.message}</Panel.Error>
|
||||
</label>
|
||||
<label>
|
||||
|
||||
+12
-15
@@ -1,5 +1,4 @@
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Switch } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { editAutomationSettings } from '../../../../common/api/automation';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
@@ -7,6 +6,7 @@ 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 Switch from '../../../../common/components/switch/Switch';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
||||
import { isOntimeCloud } from '../../../../externals';
|
||||
@@ -24,11 +24,12 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
||||
const { enabledAutomations, enabledOscIn, oscPortIn } = props;
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
register,
|
||||
setError,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting, isDirty, isValid },
|
||||
} = useForm<AutomationSettingsProps>({
|
||||
mode: 'onChange',
|
||||
@@ -103,12 +104,10 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
||||
description='Allow Ontime to send messages on lifecycle triggers'
|
||||
error={errors.enabledAutomations?.message}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='enabledAutomations'
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||
)}
|
||||
<Switch
|
||||
size='large'
|
||||
checked={watch('enabledAutomations')}
|
||||
onCheckedChange={(value: boolean) => setValue('enabledAutomations', value, { shouldDirty: true })}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
@@ -123,12 +122,10 @@ export default function AutomationSettingsForm(props: AutomationSettingsProps) {
|
||||
description='Allow control of Ontime through OSC'
|
||||
error={errors.enabledOscIn?.message}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='enabledOscIn'
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||
)}
|
||||
<Switch
|
||||
size='large'
|
||||
checked={watch('enabledOscIn')}
|
||||
onCheckedChange={(value: boolean) => setValue('enabledOscIn', value, { shouldDirty: true })}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
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 Select from '../../../../common/components/select/Select';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
@@ -29,6 +29,8 @@ export default function TriggerForm(props: TriggerFormProps) {
|
||||
register,
|
||||
setFocus,
|
||||
setError,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { errors, isSubmitting, isValid, isDirty },
|
||||
} = useForm<TriggerDTO>({
|
||||
defaultValues: {
|
||||
@@ -44,8 +46,7 @@ export default function TriggerForm(props: TriggerFormProps) {
|
||||
// give initial focus to the title field
|
||||
useEffect(() => {
|
||||
setFocus('title');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- focus on mount
|
||||
}, []);
|
||||
}, [setFocus]);
|
||||
|
||||
const onSubmit = async (values: TriggerDTO) => {
|
||||
// if we were passed an ID we are editing a Trigger
|
||||
@@ -97,33 +98,21 @@ export default function TriggerForm(props: TriggerFormProps) {
|
||||
<label>
|
||||
Lifecycle trigger
|
||||
<Select
|
||||
size='sm'
|
||||
variant='ontime'
|
||||
defaultValue={initialTrigger}
|
||||
{...register('trigger', { required: { value: true, message: 'Required field' } })}
|
||||
>
|
||||
{cycles.map((cycle) => (
|
||||
<option key={cycle.id} value={cycle.value}>
|
||||
{cycle.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
value={watch('trigger')}
|
||||
onValueChange={(value) => setValue('trigger', value as TimerLifeCycle, { shouldDirty: true })}
|
||||
options={cycles.map((cycle) => ({ value: cycle.value, label: cycle.label }))}
|
||||
aria-label='Lifecycle trigger'
|
||||
/>
|
||||
<Panel.Error>{errors.trigger?.message}</Panel.Error>
|
||||
</label>
|
||||
<label>
|
||||
Automation title
|
||||
<Select
|
||||
size='sm'
|
||||
variant='ontime'
|
||||
defaultValue={initialAutomationId}
|
||||
{...register('automationId', { required: { value: true, message: 'Required field' } })}
|
||||
>
|
||||
{automationSelect.map((automation) => (
|
||||
<option key={automation.value} value={automation.value}>
|
||||
{automation.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
value={watch('automationId')}
|
||||
onValueChange={(value) => setValue('automationId', value, { shouldDirty: true })}
|
||||
options={automationSelect}
|
||||
aria-label='Automation title'
|
||||
/>
|
||||
<Panel.Error>{errors.automationId?.message}</Panel.Error>
|
||||
</label>
|
||||
<Panel.InlineElements align='end'>
|
||||
|
||||
@@ -107,11 +107,17 @@ export default function GenerateLinkForm({ hostOptions, pathOptions, isLockedToV
|
||||
title='Lock navigation'
|
||||
description='Prevent showing navigation (will only work for non production URLs)'
|
||||
/>
|
||||
<Switch name='lock' checked={watch('lock')} onCheckedChange={(checked) => setValue('lock', checked)} />
|
||||
<Switch
|
||||
size='large'
|
||||
name='lock'
|
||||
checked={watch('lock')}
|
||||
onCheckedChange={(checked) => setValue('lock', checked)}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Authenticate' description='Whether the URL should be pre-authenticated' />
|
||||
<Switch
|
||||
size='large'
|
||||
name='authenticate'
|
||||
checked={watch('authenticate')}
|
||||
onCheckedChange={(checked) => setValue('authenticate', checked)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { IoAdd, IoOpenOutline, IoTrash } from 'react-icons/io5';
|
||||
import { Switch } from '@chakra-ui/react';
|
||||
import { URLPreset } from 'ontime-types';
|
||||
|
||||
import { postUrlPresets } from '../../../../common/api/urlPresets';
|
||||
@@ -11,6 +10,7 @@ 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 Switch from '../../../../common/components/switch/Switch';
|
||||
import Tooltip from '../../../../common/components/tooltip/Tooltip';
|
||||
import useUrlPresets from '../../../../common/hooks-query/useUrlPresets';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
@@ -34,6 +34,8 @@ export default function UrlPresetsForm() {
|
||||
register,
|
||||
reset,
|
||||
setError,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { isSubmitting, isDirty, isValid, errors },
|
||||
} = useForm<FormData>({
|
||||
mode: 'onChange',
|
||||
@@ -166,8 +168,11 @@ export default function UrlPresetsForm() {
|
||||
<tr key={preset.id}>
|
||||
<td className={style.fit}>
|
||||
<Switch
|
||||
{...register(`data.${index}.enabled`)}
|
||||
variant='ontime'
|
||||
size='large'
|
||||
checked={watch(`data.${index}.enabled`)}
|
||||
onCheckedChange={(value: boolean) =>
|
||||
setValue(`data.${index}.enabled`, value, { shouldDirty: true })
|
||||
}
|
||||
data-testid={`field__enable_${index}`}
|
||||
/>
|
||||
</td>
|
||||
|
||||
+24
-34
@@ -1,8 +1,9 @@
|
||||
import { Select, Switch } from '@chakra-ui/react';
|
||||
import { EndAction, TimerType, TimeStrategy } from 'ontime-types';
|
||||
import { parseUserTime } from 'ontime-utils';
|
||||
|
||||
import TimeInput from '../../../../common/components/input/time-input/TimeInput';
|
||||
import Select from '../../../../common/components/select/Select';
|
||||
import Switch from '../../../../common/components/switch/Switch';
|
||||
import { editorSettingsDefaults, useEditorSettings } from '../../../../common/stores/editorSettings';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
@@ -41,12 +42,7 @@ export default function RundownDefaultSettings() {
|
||||
title='Link previous'
|
||||
description='Whether the start time of new events should be linked to the previous event end time'
|
||||
/>
|
||||
<Switch
|
||||
variant='ontime'
|
||||
size='lg'
|
||||
defaultChecked={linkPrevious}
|
||||
onChange={(event) => setLinkPrevious(event.target.checked)}
|
||||
/>
|
||||
<Switch size='large' checked={linkPrevious} onCheckedChange={setLinkPrevious} />
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
@@ -54,15 +50,13 @@ export default function RundownDefaultSettings() {
|
||||
description='Which time should be maintained when event schedule is recalculated'
|
||||
/>
|
||||
<Select
|
||||
variant='ontime'
|
||||
size='sm'
|
||||
width='auto'
|
||||
value={defaultTimeStrategy}
|
||||
onChange={(event) => setTimeStrategy(event.target.value as TimeStrategy)}
|
||||
>
|
||||
<option value={TimeStrategy.LockDuration}>Duration</option>
|
||||
<option value={TimeStrategy.LockEnd}>End Time</option>
|
||||
</Select>
|
||||
onValueChange={(value) => setTimeStrategy(value as TimeStrategy)}
|
||||
options={[
|
||||
{ value: TimeStrategy.LockDuration, label: 'Duration' },
|
||||
{ value: TimeStrategy.LockEnd, label: 'End Time' },
|
||||
]}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
<Panel.ListGroup>
|
||||
@@ -78,31 +72,27 @@ export default function RundownDefaultSettings() {
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='Timer type' description='Default type of timer for new events' />
|
||||
<Select
|
||||
variant='ontime'
|
||||
size='sm'
|
||||
width='auto'
|
||||
value={defaultTimerType}
|
||||
onChange={(event) => setDefaultTimerType(event.target.value as TimerType)}
|
||||
>
|
||||
<option value={TimerType.CountDown}>Count down</option>
|
||||
<option value={TimerType.CountUp}>Count up</option>
|
||||
<option value={TimerType.Clock}>Clock</option>
|
||||
<option value={TimerType.None}>None</option>
|
||||
</Select>
|
||||
onValueChange={(value) => setDefaultTimerType(value as TimerType)}
|
||||
options={[
|
||||
{ value: TimerType.CountDown, label: 'Count down' },
|
||||
{ value: TimerType.CountUp, label: 'Count up' },
|
||||
{ value: TimerType.Clock, label: 'Clock' },
|
||||
{ value: TimerType.None, label: 'None' },
|
||||
]}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field title='End Action' description='Default end action for new events' />
|
||||
<Select
|
||||
variant='ontime'
|
||||
size='sm'
|
||||
width='auto'
|
||||
value={defaultEndAction}
|
||||
onChange={(event) => setDefaultEndAction(event.target.value as EndAction)}
|
||||
>
|
||||
<option value={EndAction.None}>None</option>
|
||||
<option value={EndAction.LoadNext}>Load next</option>
|
||||
<option value={EndAction.PlayNext}>Play next</option>
|
||||
</Select>
|
||||
onValueChange={(value) => setDefaultEndAction(value as EndAction)}
|
||||
options={[
|
||||
{ value: EndAction.None, label: 'None' },
|
||||
{ value: EndAction.LoadNext, label: 'Load next' },
|
||||
{ value: EndAction.PlayNext, label: 'Play next' },
|
||||
]}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
</Panel.ListGroup>
|
||||
<Panel.ListGroup>
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ export default function CustomFieldEntry(props: CustomFieldEntryProps) {
|
||||
</td>
|
||||
<td className={style.halfWidth}>{label}</td>
|
||||
<td className={style.fullWidth}>
|
||||
<CopyTag label='Copy key to use in integrations' copyValue={fieldKey}>
|
||||
<CopyTag size='small' copyValue={fieldKey}>
|
||||
{fieldKey}
|
||||
</CopyTag>
|
||||
</td>
|
||||
|
||||
+13
-15
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Radio, RadioGroup } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { CustomField } from 'ontime-types';
|
||||
import { customFieldLabelToKey, isAlphanumericWithSpace } from 'ontime-utils';
|
||||
|
||||
@@ -9,6 +8,7 @@ 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 RadioGroup from '../../../../../common/components/radio-group/RadioGroup';
|
||||
import useCustomFields from '../../../../../common/hooks-query/useCustomFields';
|
||||
import { preventEscape } from '../../../../../common/utils/keyEvent';
|
||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||
@@ -33,13 +33,13 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
||||
const [_, setColour] = useState(initialColour || '');
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
register,
|
||||
setFocus,
|
||||
setError,
|
||||
setValue,
|
||||
getValues,
|
||||
watch,
|
||||
formState: { errors, isSubmitting, isValid, isDirty },
|
||||
} = useForm<CustomFieldFormData>({
|
||||
defaultValues: { type: 'string', label: initialLabel || '', colour: initialColour || '' },
|
||||
@@ -90,17 +90,15 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
||||
</Info>
|
||||
<div>
|
||||
<Panel.Description>Type</Panel.Description>
|
||||
<Controller
|
||||
name='type'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RadioGroup {...field} size='sm' isDisabled={isEditMode} variant='ontime'>
|
||||
<Panel.InlineElements relation='component'>
|
||||
<Radio value='string'>Text</Radio>
|
||||
<Radio value='image'>Image</Radio>
|
||||
</Panel.InlineElements>
|
||||
</RadioGroup>
|
||||
)}
|
||||
<RadioGroup
|
||||
orientation='horizontal'
|
||||
disabled={isEditMode}
|
||||
onValueChange={(value) => setValue('type', value, { shouldDirty: true })}
|
||||
value={watch('type')}
|
||||
items={[
|
||||
{ value: 'string', label: 'Text' },
|
||||
{ value: 'image', label: 'Image' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className={style.twoCols}>
|
||||
@@ -138,7 +136,7 @@ export default function CustomFieldForm(props: CustomFieldsFormProps) {
|
||||
<Button variant='ghosted' onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type='submit' variant='primary' disabled={!canSubmit} loading={isSubmitting}>
|
||||
<Button type='submit' variant='primary' disabled={!canSubmit} loading={isSubmitting}>
|
||||
Save
|
||||
</Button>
|
||||
</Panel.InlineElements>
|
||||
|
||||
+1
-1
@@ -174,7 +174,7 @@ export default function GSheetSetup(props: GSheetSetupProps) {
|
||||
<Panel.ListGroup>
|
||||
<Panel.InlineElements>
|
||||
{isAuthenticating && <span>Authenticating...</span>}
|
||||
<CopyTag copyValue={authKey ?? ''} label='Google Auth Key' disabled={!canAuthenticate} size='sm'>
|
||||
<CopyTag copyValue={authKey ?? ''} disabled={!canAuthenticate}>
|
||||
{authKey ? authKey : 'Upload files to generate Auth Key'}
|
||||
</CopyTag>
|
||||
<Button onClick={handleAuthenticate} disabled={!canAuthenticate}>
|
||||
|
||||
+8
-13
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { IoAdd, IoTrash } from 'react-icons/io5';
|
||||
import { Select } from '@chakra-ui/react';
|
||||
import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils';
|
||||
|
||||
import Button from '../../../../../../common/components/buttons/Button';
|
||||
import IconButton from '../../../../../../common/components/buttons/IconButton';
|
||||
import Input from '../../../../../../common/components/input/input/Input';
|
||||
import Select from '../../../../../../common/components/select/Select';
|
||||
import Tooltip from '../../../../../../common/components/tooltip/Tooltip';
|
||||
import * as Panel from '../../../../panel-utils/PanelUtils';
|
||||
import useGoogleSheet from '../useGoogleSheet';
|
||||
@@ -33,6 +33,7 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
||||
handleSubmit,
|
||||
register,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors, isValid },
|
||||
} = useForm<NamedImportMap>({
|
||||
mode: 'onChange',
|
||||
@@ -149,19 +150,13 @@ export default function ImportMapForm(props: ImportMapFormProps) {
|
||||
<td>{label}</td>
|
||||
<td>
|
||||
<Select
|
||||
variant='ontime'
|
||||
id={importName as string}
|
||||
size='sm'
|
||||
{...register(label as keyof NamedImportMap)}
|
||||
>
|
||||
{worksheetNames?.map((name) => {
|
||||
return (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
value={watch(label as keyof NamedImportMap) as string}
|
||||
onValueChange={(value: string) =>
|
||||
setValue(label as keyof NamedImportMap, value, { shouldDirty: true })
|
||||
}
|
||||
options={worksheetNames?.map((name) => ({ value: name, label: name })) || []}
|
||||
/>
|
||||
</td>
|
||||
<td className={style.singleActionCell} />
|
||||
</tr>
|
||||
|
||||
@@ -21,12 +21,7 @@ export default function InfoNif() {
|
||||
const address = linkToOtherHost(nif.address);
|
||||
|
||||
return (
|
||||
<CopyTag
|
||||
key={nif.name}
|
||||
copyValue={address}
|
||||
onClick={() => handleClick(address)}
|
||||
label='Copy IP or navigate to address'
|
||||
>
|
||||
<CopyTag key={nif.name} copyValue={address} onClick={() => handleClick(address)}>
|
||||
{`${nif.name} - ${nif.address}`} <IoArrowUp className={style.goIcon} />
|
||||
</CopyTag>
|
||||
);
|
||||
|
||||
+5
@@ -10,3 +10,8 @@
|
||||
width: 50%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.copiable {
|
||||
cursor: text ;
|
||||
user-select: text;
|
||||
}
|
||||
+3
-3
@@ -61,8 +61,8 @@ export default function ClientList() {
|
||||
<Panel.Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td className={style.halfWidth}>Client Name</td>
|
||||
<td className={style.fullWidth}>Path</td>
|
||||
<td style={{ width: '20%' }}>Client Name</td>
|
||||
<td>Path</td>
|
||||
<td />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -76,7 +76,7 @@ export default function ClientList() {
|
||||
{isCurrent && <Tag>SELF</Tag>}
|
||||
{name}
|
||||
</Panel.InlineElements>
|
||||
<td>{path}</td>
|
||||
<td className={style.copiable}>{path}</td>
|
||||
<Panel.InlineElements relation='inner'>
|
||||
<Button
|
||||
size='small'
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { IoEllipsisHorizontal } from 'react-icons/io5';
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import {
|
||||
IoCopyOutline,
|
||||
IoDocumentOutline,
|
||||
IoDownloadOutline,
|
||||
IoEllipsisHorizontal,
|
||||
IoPencilOutline,
|
||||
IoTrash,
|
||||
} from 'react-icons/io5';
|
||||
|
||||
import {
|
||||
deleteProject,
|
||||
@@ -11,6 +17,8 @@ import {
|
||||
renameProject,
|
||||
} from '../../../../common/api/db';
|
||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||
import IconButton from '../../../../common/components/buttons/IconButton';
|
||||
import { DropdownMenu } from '../../../../common/components/dropdown-menu/DropdownMenu';
|
||||
import { cx } from '../../../../common/utils/styleUtils';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
@@ -184,31 +192,33 @@ function ActionMenu(props: ActionMenuProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu variant='ontime-on-dark' size='sm'>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
aria-label='Options'
|
||||
icon={<IoEllipsisHorizontal />}
|
||||
color='#e2e2e2' // $gray-200
|
||||
variant='ontime-ghosted'
|
||||
size='sm'
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
<MenuList>
|
||||
<MenuItem onClick={() => onLoad(filename)} isDisabled={current}>
|
||||
Load
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => onMerge(filename)} isDisabled={current}>
|
||||
Partial Load
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
||||
<MenuItem onClick={handleDuplicate}>Duplicate</MenuItem>
|
||||
<MenuItem onClick={handleDownload}>Download</MenuItem>
|
||||
<MenuItem onClick={handleExportCSV}>Export CSV Rundown</MenuItem>
|
||||
<MenuItem isDisabled={current} onClick={() => onDelete(filename)}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
<DropdownMenu
|
||||
render={<IconButton variant='ghosted-white' />}
|
||||
disabled={isDisabled}
|
||||
items={[
|
||||
{
|
||||
type: 'item',
|
||||
icon: IoDownloadOutline,
|
||||
label: 'Load',
|
||||
onClick: () => onLoad(filename),
|
||||
disabled: current,
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
icon: IoDownloadOutline,
|
||||
label: 'Partial Load',
|
||||
onClick: () => onMerge(filename),
|
||||
disabled: current,
|
||||
},
|
||||
{ type: 'item', icon: IoPencilOutline, label: 'Rename', onClick: handleRename },
|
||||
{ type: 'item', icon: IoCopyOutline, label: 'Duplicate', onClick: handleDuplicate },
|
||||
{ type: 'item', icon: IoDocumentOutline, label: 'Download', onClick: handleDownload },
|
||||
{ type: 'item', icon: IoDocumentOutline, label: 'Export CSV Rundown', onClick: handleExportCSV },
|
||||
{ type: 'divider' },
|
||||
{ type: 'item', icon: IoTrash, label: 'Delete', onClick: () => onDelete(filename), disabled: current },
|
||||
]}
|
||||
>
|
||||
<IoEllipsisHorizontal />
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
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 Switch from '../../../../common/components/switch/Switch';
|
||||
import { cx } from '../../../../common/utils/styleUtils';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
|
||||
@@ -34,7 +34,8 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
watch,
|
||||
setValue,
|
||||
formState: { isSubmitting, isValid, isDirty },
|
||||
} = useForm<ProjectMergeFormValues>({
|
||||
defaultValues: {
|
||||
@@ -92,23 +93,43 @@ export default function ProjectMergeForm(props: ProjectMergeFromProps) {
|
||||
<br /> This process is irreversible.
|
||||
</Panel.Description>
|
||||
<label>
|
||||
<Switch variant='ontime' {...register('project')} />
|
||||
<Switch
|
||||
size='large'
|
||||
checked={watch('project')}
|
||||
onCheckedChange={(value: boolean) => setValue('project', value, { shouldDirty: true })}
|
||||
/>
|
||||
Project data
|
||||
</label>
|
||||
<label>
|
||||
<Switch variant='ontime' {...register('rundown')} />
|
||||
<Switch
|
||||
size='large'
|
||||
checked={watch('rundown')}
|
||||
onCheckedChange={(value: boolean) => setValue('rundown', value, { shouldDirty: true })}
|
||||
/>
|
||||
Rundown + Custom Fields
|
||||
</label>
|
||||
<label>
|
||||
<Switch variant='ontime' {...register('viewSettings')} />
|
||||
<Switch
|
||||
size='large'
|
||||
checked={watch('viewSettings')}
|
||||
onCheckedChange={(value: boolean) => setValue('viewSettings', value, { shouldDirty: true })}
|
||||
/>
|
||||
View Settings
|
||||
</label>
|
||||
<label>
|
||||
<Switch variant='ontime' {...register('urlPresets')} />
|
||||
<Switch
|
||||
size='large'
|
||||
checked={watch('urlPresets')}
|
||||
onCheckedChange={(value: boolean) => setValue('urlPresets', value, { shouldDirty: true })}
|
||||
/>
|
||||
URL Presets
|
||||
</label>
|
||||
<label>
|
||||
<Switch variant='ontime' {...register('automation')} />
|
||||
<Switch
|
||||
size='large'
|
||||
checked={watch('automation')}
|
||||
onCheckedChange={(value: boolean) => setValue('automation', value, { shouldDirty: true })}
|
||||
/>
|
||||
Automation Settings
|
||||
</label>
|
||||
</Panel.Section>
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function GeneralSettings() {
|
||||
description='Protect the editor view with a pin code'
|
||||
error={errors.editorKey?.message}
|
||||
/>
|
||||
<GeneralPinInput register={register} formName='editorKey' isDisabled={disableInputs} />
|
||||
<GeneralPinInput register={register} formName='editorKey' disabled={disableInputs} />
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
@@ -127,7 +127,7 @@ export default function GeneralSettings() {
|
||||
description='Protect the operator and cuesheet views with a pin code'
|
||||
error={errors.operatorKey?.message}
|
||||
/>
|
||||
<GeneralPinInput register={register} formName='operatorKey' isDisabled={disableInputs} />
|
||||
<GeneralPinInput register={register} formName='operatorKey' disabled={disableInputs} />
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
<Panel.Field
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { Switch } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { ViewSettings as ViewSettingsType } from 'ontime-types';
|
||||
|
||||
@@ -10,6 +9,7 @@ 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 Switch from '../../../../common/components/switch/Switch';
|
||||
import useViewSettings from '../../../../common/hooks-query/useViewSettings';
|
||||
import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
@@ -28,6 +28,8 @@ export default function ViewSettings() {
|
||||
setError,
|
||||
register,
|
||||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { isSubmitting, isDirty, errors },
|
||||
} = useForm<ViewSettingsType>({
|
||||
defaultValues: data,
|
||||
@@ -96,12 +98,10 @@ export default function ViewSettings() {
|
||||
title='Override CSS styles'
|
||||
description='Enables overriding view styles with custom stylesheet'
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='overrideStyles'
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||
)}
|
||||
<Switch
|
||||
size='large'
|
||||
checked={watch('overrideStyles')}
|
||||
onCheckedChange={(value: boolean) => setValue('overrideStyles', value, { shouldDirty: true })}
|
||||
/>
|
||||
<Button onClick={codeEditorHandler.open} disabled={isSubmitting}>
|
||||
Edit CSS override
|
||||
@@ -128,12 +128,10 @@ export default function ViewSettings() {
|
||||
title='Freeze timer on end'
|
||||
description='When a timer hits 00:00:00, it freezes instead of going negative. It invalidates the End Message.'
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name='freezeEnd'
|
||||
render={({ field: { onChange, value, ref } }) => (
|
||||
<Switch variant='ontime' size='lg' isChecked={value} onChange={onChange} ref={ref} />
|
||||
)}
|
||||
<Switch
|
||||
size='large'
|
||||
checked={watch('freezeEnd')}
|
||||
onCheckedChange={(value: boolean) => setValue('freezeEnd', value, { shouldDirty: true })}
|
||||
/>
|
||||
</Panel.ListItem>
|
||||
<Panel.ListItem>
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
input {
|
||||
width: 5em;
|
||||
}
|
||||
}
|
||||
+18
-18
@@ -1,24 +1,26 @@
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { UseFormRegister } from 'react-hook-form';
|
||||
import { IoEyeOutline } from 'react-icons/io5';
|
||||
import { IconButton, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
|
||||
import { Settings } from 'ontime-types';
|
||||
|
||||
import IconButton from '../../../../../common/components/buttons/IconButton';
|
||||
import Input from '../../../../../common/components/input/input/Input';
|
||||
import { isAlphanumeric } from '../../../../../common/utils/regex';
|
||||
|
||||
import style from './GeneralPinInput.module.scss';
|
||||
|
||||
interface GeneralPinInputProps {
|
||||
register: UseFormRegister<Settings>;
|
||||
formName: keyof Settings;
|
||||
isDisabled?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function GeneralPinInput(props: PropsWithChildren<GeneralPinInputProps>) {
|
||||
const { register, formName, isDisabled } = props;
|
||||
export default function GeneralPinInput({ register, formName, disabled }: PropsWithChildren<GeneralPinInputProps>) {
|
||||
const [isVisible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<InputGroup size='sm' width='100px'>
|
||||
<div className={style.container}>
|
||||
<Input
|
||||
variant='ontime-filled'
|
||||
type={isVisible ? 'text' : 'password'}
|
||||
maxLength={4}
|
||||
{...register(formName, {
|
||||
@@ -28,18 +30,16 @@ export default function GeneralPinInput(props: PropsWithChildren<GeneralPinInput
|
||||
},
|
||||
})}
|
||||
placeholder='-'
|
||||
isDisabled={isDisabled}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<InputRightElement>
|
||||
<IconButton
|
||||
onMouseDown={() => setVisible(true)}
|
||||
onMouseUp={() => setVisible(false)}
|
||||
size='sm'
|
||||
variant='ontime-ghosted'
|
||||
icon={<IoEyeOutline />}
|
||||
aria-label='Show pin code'
|
||||
/>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
<IconButton
|
||||
onMouseDown={() => setVisible(true)}
|
||||
onMouseUp={() => setVisible(false)}
|
||||
variant='ghosted'
|
||||
aria-label='Show pin code'
|
||||
>
|
||||
<IoEyeOutline />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,8 @@ export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
||||
error={errors.settings?.timeFormat?.message}
|
||||
/>
|
||||
<Select
|
||||
{...register('settings.timeFormat')}
|
||||
value={watch('settings.timeFormat')}
|
||||
onValueChange={(value: '12' | '24') => setValue('settings.timeFormat', value, { shouldDirty: true })}
|
||||
defaultValue='24'
|
||||
options={[
|
||||
{ value: '12', label: '12 hours 11:00:10 PM' },
|
||||
@@ -90,7 +91,8 @@ export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
||||
error={errors.settings?.language?.message}
|
||||
/>
|
||||
<Select
|
||||
{...register('settings.language')}
|
||||
value={watch('settings.language')}
|
||||
onValueChange={(value: string) => setValue('settings.language', value, { shouldDirty: true })}
|
||||
defaultValue='en'
|
||||
options={[
|
||||
{ value: 'en', label: 'English' },
|
||||
@@ -132,6 +134,7 @@ export default function QuickStart({ isOpen, onClose }: QuickStartProps) {
|
||||
description='When a timer hits 00:00:00, it freezes instead of going negative. It invalidates the End Message.'
|
||||
/>
|
||||
<Switch
|
||||
size='large'
|
||||
name='viewSettings.freezeEnd'
|
||||
checked={watch('viewSettings.freezeEnd')}
|
||||
onCheckedChange={(checked) => setValue('viewSettings.freezeEnd', checked)}
|
||||
|
||||
@@ -2,16 +2,6 @@ $button-bg-gray: $gray-1050;
|
||||
$button-color-white: $gray-50;
|
||||
|
||||
@mixin tap-factory($theme-color) {
|
||||
font-family: $ontime-font-family;
|
||||
font-size: calc(1rem + 2px);
|
||||
border-radius: $component-border-radius-md;
|
||||
width: 100%;
|
||||
transition-property: color, background-color;
|
||||
transition-duration: $transition-time-feedback;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
background-color: $button-bg-gray;
|
||||
color: $theme-color;
|
||||
|
||||
@@ -43,6 +33,18 @@ $button-color-white: $gray-50;
|
||||
}
|
||||
}
|
||||
|
||||
.tapButton {
|
||||
font-family: $ontime-font-family;
|
||||
font-size: calc(1rem + 2px);
|
||||
border-radius: $component-border-radius-md;
|
||||
width: 100%;
|
||||
transition-property: color, background-color;
|
||||
transition-duration: $transition-time-feedback;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.tapButton.neutral {
|
||||
@include tap-factory($gray-50);
|
||||
|
||||
@@ -86,7 +88,7 @@ $button-color-white: $gray-50;
|
||||
|
||||
.tapButton.tight {
|
||||
padding-inline: 0.5rem;
|
||||
width: fit-content
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.tapButton.fill {
|
||||
|
||||
@@ -17,10 +17,15 @@ interface TapButtonProps {
|
||||
|
||||
const TapButton = forwardRef((props: PropsWithChildren<TapButtonProps>, ref: ForwardedRef<HTMLButtonElement>) => {
|
||||
const { children, disabled, onClick, theme = 'neutral', aspect = 'normal', active, className } = props;
|
||||
const classes = cx([style.tapButton, className, style[theme], style[aspect], active ? style.active : null]);
|
||||
|
||||
return (
|
||||
<button className={classes} disabled={disabled} type='button' onClick={onClick} ref={ref}>
|
||||
<button
|
||||
className={cx([style.tapButton, className, style[theme], style[aspect], active && style.active])}
|
||||
disabled={disabled}
|
||||
type='button'
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,12 @@ $info-hover: $section-white;
|
||||
user-select: text;
|
||||
min-height: 20vh;
|
||||
max-height: 40vh;
|
||||
background-color: $gray-1350;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.apart {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.logEntry {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { IoClose } from 'react-icons/io5';
|
||||
import { LogOrigin } from 'ontime-types';
|
||||
|
||||
import Button from '../../common/components/buttons/Button';
|
||||
@@ -105,8 +106,8 @@ export default function Log() {
|
||||
>
|
||||
{LogOrigin.Tx}
|
||||
</Button>
|
||||
<Button variant='subtle-destructive' size='small' onClick={clearLogs}>
|
||||
Clear
|
||||
<Button variant='subtle-destructive' size='small' onClick={clearLogs} className={style.apart}>
|
||||
<IoClose /> Clear
|
||||
</Button>
|
||||
</Panel.InlineElements>
|
||||
<ul className={style.log}>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
top: 10%;
|
||||
left: 50%;
|
||||
|
||||
z-index: $zindex-dialog;
|
||||
transform: translateX(-50%);
|
||||
|
||||
padding-inline: 1rem;
|
||||
|
||||
@@ -78,7 +78,6 @@ export default function EditModal(props: EditModalProps) {
|
||||
defaultValue={field.value}
|
||||
data-field={field.id}
|
||||
disabled={loading}
|
||||
resize='none'
|
||||
rows={5}
|
||||
/>
|
||||
</Fragment>
|
||||
|
||||
@@ -12,7 +12,10 @@ import {
|
||||
import { isStringBoolean } from '../viewers/common/viewUtils';
|
||||
|
||||
export const getOperatorOptions = (customFields: CustomFields, timeFormat: string): ViewOption[] => {
|
||||
const fieldOptions = makeOptionsFromCustomFields(customFields, { title: 'Title', note: 'Note' });
|
||||
const fieldOptions = makeOptionsFromCustomFields(customFields, [
|
||||
{ value: 'title', label: 'Title' },
|
||||
{ value: 'note', label: 'Note' },
|
||||
]);
|
||||
const customFieldSelect = makeCustomFieldSelectOptions(customFields);
|
||||
|
||||
return [
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { memo } from 'react';
|
||||
import { useSessionStorage } from '@mantine/hooks';
|
||||
|
||||
import { ContextMenu } from '../../common/components/context-menu/ContextMenu';
|
||||
import { Corner } from '../../common/components/editor-utils/EditorUtils';
|
||||
import ErrorBoundary from '../../common/components/error-boundary/ErrorBoundary';
|
||||
import ViewNavigationMenu from '../../common/components/navigation-menu/ViewNavigationMenu';
|
||||
@@ -13,6 +12,7 @@ import { AppMode, sessionKeys } from '../../ontimeConfig';
|
||||
|
||||
import RundownEntryEditor from './entry-editor/RundownEntryEditor';
|
||||
import FinderPlacement from './placements/FinderPlacement';
|
||||
import { RundownContextMenu } from './rundown-context-menu/RundownContextMenu';
|
||||
import RundownWrapper from './RundownWrapper';
|
||||
|
||||
import style from './RundownExport.module.scss';
|
||||
@@ -39,9 +39,9 @@ function RundownExport() {
|
||||
<ViewNavigationMenu suppressSettings />
|
||||
<div className={style.content}>
|
||||
<ErrorBoundary>
|
||||
<ContextMenu>
|
||||
<RundownContextMenu>
|
||||
<RundownWrapper isSmallDevice />
|
||||
</ContextMenu>
|
||||
</RundownContextMenu>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,9 +59,9 @@ function RundownExport() {
|
||||
<div className={style.list}>
|
||||
<ErrorBoundary>
|
||||
<Corner onClick={(event) => handleLinks('rundown', event)} />
|
||||
<ContextMenu>
|
||||
<RundownContextMenu>
|
||||
<RundownWrapper />
|
||||
</ContextMenu>
|
||||
</RundownContextMenu>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
{!hideSideBar && (
|
||||
|
||||
@@ -16,12 +16,8 @@ function EventEditorFooter({ id, cue }: EventEditorFooterProps) {
|
||||
|
||||
return (
|
||||
<div className={style.footer}>
|
||||
<CopyTag copyValue={loadById} label='OSC trigger by ID'>
|
||||
{loadById}
|
||||
</CopyTag>
|
||||
<CopyTag copyValue={loadByCue} label='OSC trigger by cue'>
|
||||
{loadByCue}
|
||||
</CopyTag>
|
||||
<CopyTag copyValue={loadById}>{loadById}</CopyTag>
|
||||
<CopyTag copyValue={loadByCue}>{loadByCue}</CopyTag>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type CSSProperties, useCallback, useRef } from 'react';
|
||||
|
||||
import * as Editor from '../../../../common/components/editor-utils/EditorUtils';
|
||||
import { AutoTextArea } from '../../../../common/components/input/auto-text-area/AutoTextArea';
|
||||
import { AutoTextarea } from '../../../../common/components/input/auto-textarea/AutoTextarea';
|
||||
import useReactiveTextInput from '../../../../common/components/input/text-input/useReactiveTextInput';
|
||||
import { EventEditorUpdateFields } from '../EventEditor';
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function EventTextArea({
|
||||
style: givenStyles,
|
||||
submitHandler,
|
||||
}: CountedTextAreaProps) {
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const ref = useRef<HTMLTextAreaElement | null>(null);
|
||||
const submitCallback = useCallback((newValue: string) => submitHandler(field, newValue), [field, submitHandler]);
|
||||
|
||||
const { value, onChange, onBlur, onKeyDown } = useReactiveTextInput(initialValue, submitCallback, ref, {
|
||||
@@ -34,14 +34,12 @@ export default function EventTextArea({
|
||||
<Editor.Label className={className} htmlFor={field} style={givenStyles}>
|
||||
{label}
|
||||
</Editor.Label>
|
||||
<AutoTextArea
|
||||
<AutoTextarea
|
||||
id={field}
|
||||
inputref={ref}
|
||||
rows={1}
|
||||
size='sm'
|
||||
resize='none'
|
||||
variant='ontime-filled'
|
||||
data-testid='input-textarea'
|
||||
fluid
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
|
||||
@@ -67,10 +67,10 @@ function QuickAddInline({ previousEventId, parentBlock }: QuickAddInlineProps) {
|
||||
<div className={style.quickAdd} data-testid='quick-add-inline'>
|
||||
<DropdownMenu
|
||||
items={[
|
||||
{ type: 'item', icon: <IoAdd />, label: 'Add Event', onClick: addEvent },
|
||||
{ type: 'item', icon: <IoAdd />, label: 'Add Delay', onClick: addDelay },
|
||||
{ type: 'item', icon: <IoAdd />, label: 'Add Milestone', onClick: addMilestone },
|
||||
{ type: 'item', icon: <IoAdd />, label: 'Add Group', onClick: addBlock, disabled: parentBlock !== null },
|
||||
{ type: 'item', icon: IoAdd, label: 'Add Event', onClick: addEvent },
|
||||
{ type: 'item', icon: IoAdd, label: 'Add Delay', onClick: addDelay },
|
||||
{ type: 'item', icon: IoAdd, label: 'Add Milestone', onClick: addMilestone },
|
||||
{ type: 'item', icon: IoAdd, label: 'Add Group', onClick: addBlock, disabled: parentBlock !== null },
|
||||
]}
|
||||
render={<IconButton size='small' variant='primary' className={style.addButton} />}
|
||||
>
|
||||
|
||||
@@ -36,21 +36,26 @@ export default function RundownBlock({ data, hasCursor, collapsed, onCollapse }:
|
||||
|
||||
const [onContextMenu] = useContextMenu<HTMLDivElement>([
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Clone Group',
|
||||
icon: IoDuplicateOutline,
|
||||
onClick: () => clone(data.id),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
|
||||
label: 'Ungroup',
|
||||
icon: IoFolderOpenOutline,
|
||||
onClick: () => ungroup(data.id),
|
||||
isDisabled: data.entries.length === 0,
|
||||
disabled: data.entries.length === 0,
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Delete Group',
|
||||
icon: IoTrash,
|
||||
onClick: () => deleteEntry([data.id]),
|
||||
withDivider: true,
|
||||
disabled: true,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { DropdownMenuOption, PositionedDropdownMenu } from '../../../common/components/dropdown-menu/DropdownMenu';
|
||||
|
||||
type Position = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
type ContextMenuStore = {
|
||||
position: Position;
|
||||
options: DropdownMenuOption[];
|
||||
isOpen: boolean;
|
||||
setContextMenu: (position: Position, options: DropdownMenuOption[]) => void;
|
||||
setIsOpen: (newIsOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const useContextMenuStore = create<ContextMenuStore>((set) => ({
|
||||
position: { x: 0, y: 0 },
|
||||
options: [],
|
||||
isOpen: false,
|
||||
setContextMenu: (position, options) => set(() => ({ position, options, isOpen: true })),
|
||||
setIsOpen: (newIsOpen) => set(() => ({ isOpen: newIsOpen })),
|
||||
}));
|
||||
|
||||
export function RundownContextMenu({ children }: PropsWithChildren) {
|
||||
const { position, options, isOpen, setIsOpen } = useContextMenuStore();
|
||||
|
||||
const onClose = () => {
|
||||
return setIsOpen(false);
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<PositionedDropdownMenu isOpen position={position} onClose={onClose} items={options} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -107,6 +107,7 @@ export default function RundownEvent({
|
||||
selectedEvents.size > 1
|
||||
? [
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Link to previous',
|
||||
icon: IoLink,
|
||||
onClick: () =>
|
||||
@@ -116,6 +117,8 @@ export default function RundownEvent({
|
||||
}),
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
|
||||
label: 'Unlink from previous',
|
||||
icon: IoUnlink,
|
||||
onClick: () =>
|
||||
@@ -124,11 +127,15 @@ export default function RundownEvent({
|
||||
value: null,
|
||||
}),
|
||||
},
|
||||
{ withDivider: true, label: 'Group', icon: IoFolder, onClick: () => actionHandler('group') },
|
||||
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
||||
{ type: 'divider' },
|
||||
{ type: 'item', label: 'Group', icon: IoFolder, onClick: () => actionHandler('group') },
|
||||
{ type: 'divider' },
|
||||
{ type: 'item', label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
||||
]
|
||||
: [
|
||||
{
|
||||
type: 'item',
|
||||
|
||||
label: 'Toggle link to previous',
|
||||
icon: IoLink,
|
||||
onClick: () =>
|
||||
@@ -137,23 +144,34 @@ export default function RundownEvent({
|
||||
value: linkStart,
|
||||
}),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
|
||||
{
|
||||
type: 'item',
|
||||
|
||||
label: 'Add to swap',
|
||||
icon: IoAdd,
|
||||
onClick: () => setSelectedEventId(eventId),
|
||||
withDivider: true,
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
|
||||
label: `Swap this event with ${selectedEventId ?? ''}`,
|
||||
icon: IoSwapVertical,
|
||||
onClick: () => {
|
||||
actionHandler('swap', { field: 'id', value: selectedEventId });
|
||||
clearSelectedEventId();
|
||||
},
|
||||
isDisabled: selectedEventId == null || selectedEventId === eventId,
|
||||
disabled: selectedEventId == null || selectedEventId === eventId,
|
||||
},
|
||||
{ withDivider: false, label: 'Clone', icon: IoDuplicateOutline, onClick: () => actionHandler('clone') },
|
||||
{ withDivider: true, label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
||||
{
|
||||
type: 'item',
|
||||
label: 'Clone',
|
||||
icon: IoDuplicateOutline,
|
||||
onClick: () => actionHandler('clone'),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{ type: 'item', label: 'Delete', icon: IoTrash, onClick: () => actionHandler('delete') },
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -48,7 +48,11 @@ export const getClockOptions = (timeFormat: string): ViewOption[] => [
|
||||
title: 'Align Horizontal',
|
||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||
values: [
|
||||
{ value: 'start', label: 'Start' },
|
||||
{ value: 'center', label: 'Center' },
|
||||
{ value: 'end', label: 'End' },
|
||||
],
|
||||
defaultValue: 'center',
|
||||
},
|
||||
{
|
||||
@@ -63,7 +67,11 @@ export const getClockOptions = (timeFormat: string): ViewOption[] => [
|
||||
title: 'Align Vertical',
|
||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||
values: [
|
||||
{ value: 'start', label: 'Start' },
|
||||
{ value: 'center', label: 'Center' },
|
||||
{ value: 'end', label: 'End' },
|
||||
],
|
||||
defaultValue: 'center',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,16 +8,16 @@ import { makeOptionsFromCustomFields } from '../../../common/components/view-par
|
||||
import safeParseNumber from '../../../common/utils/safeParseNumber';
|
||||
|
||||
export const getLowerThirdOptions = (customFields: CustomFields): ViewOption[] => {
|
||||
const topSourceOptions = makeOptionsFromCustomFields(customFields, {
|
||||
title: 'Title',
|
||||
note: 'Note',
|
||||
});
|
||||
const topSourceOptions = makeOptionsFromCustomFields(customFields, [
|
||||
{ value: 'title', label: 'Title' },
|
||||
{ value: 'note', label: 'Note' },
|
||||
]);
|
||||
|
||||
const bottomSourceOptions = makeOptionsFromCustomFields(customFields, {
|
||||
title: 'Title',
|
||||
note: 'Note',
|
||||
none: 'None',
|
||||
});
|
||||
const bottomSourceOptions = makeOptionsFromCustomFields(customFields, [
|
||||
{ value: 'title', label: 'Title' },
|
||||
{ value: 'note', label: 'Note' },
|
||||
{ value: 'none', label: 'None' },
|
||||
]);
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -99,10 +99,9 @@ export const getLowerThirdOptions = (customFields: CustomFields): ViewOption[] =
|
||||
{
|
||||
id: 'width',
|
||||
title: 'Minimum Width',
|
||||
description: 'Minimum Width of the element',
|
||||
description: 'Minimum Width of the element (percentage)',
|
||||
type: 'number',
|
||||
prefix: '%',
|
||||
placeholder: '45 (default)',
|
||||
placeholder: '45 (default %)',
|
||||
},
|
||||
{
|
||||
id: 'key',
|
||||
|
||||
@@ -68,7 +68,11 @@ export const MINIMAL_TIMER_OPTIONS: ViewOption[] = [
|
||||
title: 'Align Horizontal',
|
||||
description: 'Moves the horizontally in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||
values: [
|
||||
{ value: 'start', label: 'Start' },
|
||||
{ value: 'center', label: 'Center' },
|
||||
{ value: 'end', label: 'End' },
|
||||
],
|
||||
defaultValue: 'center',
|
||||
},
|
||||
{
|
||||
@@ -83,7 +87,11 @@ export const MINIMAL_TIMER_OPTIONS: ViewOption[] = [
|
||||
title: 'Align Vertical',
|
||||
description: 'Moves the vertically in page to start = left | center | end = right',
|
||||
type: 'option',
|
||||
values: { start: 'Start', center: 'Center', end: 'End' },
|
||||
values: [
|
||||
{ value: 'start', label: 'Start' },
|
||||
{ value: 'center', label: 'Center' },
|
||||
{ value: 'end', label: 'End' },
|
||||
],
|
||||
defaultValue: 'center',
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user