diff --git a/apps/client/src/common/api/ontimeApi.ts b/apps/client/src/common/api/ontimeApi.ts index a826d4981..c45e63cd1 100644 --- a/apps/client/src/common/api/ontimeApi.ts +++ b/apps/client/src/common/api/ontimeApi.ts @@ -1,5 +1,13 @@ import axios from 'axios'; -import { Alias, EventData, OSCSettings, Settings, UserFields, ViewSettings } from 'ontime-types'; +import { + Alias, + EventData, + OSCSettings, + OscSubscription, + Settings, + UserFields, + ViewSettings, +} from 'ontime-types'; import { apiRepoLatest } from '../../externals'; import { InfoType } from '../models/Info'; @@ -100,6 +108,14 @@ export async function postOSC(data: OSCSettings) { return axios.post(`${ontimeURL}/osc`, data); } +/** + * @description HTTP request to mutate osc subscriptions + * @return {Promise} + */ +export async function postOscSubscriptions(data: OscSubscription) { + return axios.post(`${ontimeURL}/osc-subscriptions`, data); +} + /** * @description HTTP request to download db * @return {Promise} diff --git a/apps/client/src/common/hooks-query/useOscSettings.ts b/apps/client/src/common/hooks-query/useOscSettings.ts index 4e58c0e33..c7bc15fd9 100644 --- a/apps/client/src/common/hooks-query/useOscSettings.ts +++ b/apps/client/src/common/hooks-query/useOscSettings.ts @@ -3,7 +3,7 @@ import { OSCSettings } from 'ontime-types'; import { queryRefetchIntervalSlow } from '../../ontimeConfig'; import { OSC_SETTINGS } from '../api/apiConstants'; -import { getOSC, postOSC } from '../api/ontimeApi'; +import { getOSC, postOSC, postOscSubscriptions } from '../api/ontimeApi'; import { oscPlaceholderSettings } from '../models/OscSettings'; import { ontimeQueryClient } from '../queryClient'; @@ -18,6 +18,7 @@ export default function useOscSettings() { networkMode: 'always', }); + // we need to jump through some hoops because of the type op port return { data: data! as unknown as OSCSettings, status, isError, refetch }; } @@ -29,3 +30,11 @@ export function useOscSettingsMutation() { }); return { isLoading, mutateAsync }; } + +export function usePostOscSubscriptions() { + const { isLoading, mutateAsync } = useMutation({ + mutationFn: postOscSubscriptions, + onSettled: () => ontimeQueryClient.invalidateQueries({ queryKey: OSC_SETTINGS }), + }); + return { isLoading, mutateAsync }; +} diff --git a/apps/client/src/features/modals/integration-modal/OscIntegration.tsx b/apps/client/src/features/modals/integration-modal/OscIntegration.tsx index d63417c25..12fc1c059 100644 --- a/apps/client/src/features/modals/integration-modal/OscIntegration.tsx +++ b/apps/client/src/features/modals/integration-modal/OscIntegration.tsx @@ -1,11 +1,9 @@ import { useState } from 'react'; import { useForm } from 'react-hook-form'; -import type { OSCSettings, OscSubscription } from 'ontime-types'; +import type { OscSubscription } from 'ontime-types'; import { TimerLifeCycle } from 'ontime-types'; -import { generateId } from 'ontime-utils'; -import useOscSettings, { useOscSettingsMutation } from '../../../common/hooks-query/useOscSettings'; -import { oscPlaceholderSettings, PlaceholderSettings } from '../../../common/models/OscSettings'; +import useOscSettings, { usePostOscSubscriptions } from '../../../common/hooks-query/useOscSettings'; import { useEmitLog } from '../../../common/stores/logger'; import OntimeModalFooter from '../OntimeModalFooter'; @@ -44,87 +42,102 @@ const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string export default function OscIntegration() { const { data } = useOscSettings(); - const { mutateAsync } = useOscSettingsMutation(); + const { mutateAsync } = usePostOscSubscriptions(); const { emitError } = useEmitLog(); const { + control, handleSubmit, register, reset, formState: { isSubmitting, isDirty, isValid }, - } = useForm({ - defaultValues: data, - values: data, + } = useForm({ + defaultValues: data.subscriptions, + values: data.subscriptions, }); - const [subscriptionState, setSubscription] = useState( - data?.subscriptions || oscPlaceholderSettings.subscriptions, - ); - const [hasManualChange, setHasManualChange] = useState(false); - const [showSection, setShowSection] = useState(TimerLifeCycle.onLoad); const resetForm = () => { - const originalData = data || oscPlaceholderSettings; - setSubscription(originalData.subscriptions); - // @ts-expect-error -- we know the data here is safe - reset(originalData); + reset(data.subscriptions); }; - const deleteSubscriptionEntry = (cycle: OntimeCycle, id: string) => { - setSubscription((prev) => { - const newData = { ...prev }; - newData[cycle] = [...prev[cycle].filter((el) => el.id !== id)]; - return newData; - }); - setHasManualChange(true); - }; - - const addNewSubscriptionEntry = async (cycle: OntimeCycle) => { - setSubscription((prev) => { - const newData = structuredClone(prev); - newData[cycle] = [...prev[cycle], { id: generateId(), message: '', enabled: false }]; - return newData; - }); - setHasManualChange(true); - }; - - const onSubmit = async (values: OSCSettings | PlaceholderSettings) => { + const onSubmit = async (values: OscSubscription) => { try { - // @ts-expect-error -- we know of the type mismatch, not pertinent here - await mutateAsync(values); - setHasManualChange(false); + const subscriptions = { + onLoad: values.onLoad ?? [], + onStart: values.onStart ?? [], + onPause: values.onPause ?? [], + onStop: values.onStop ?? [], + onUpdate: values.onUpdate ?? [], + onFinish: values.onFinish ?? [], + }; + + await mutateAsync(subscriptions); } catch (error) { emitError(`Error setting OSC: ${error}`); } }; - const subscriptionKeys = Object.keys(subscriptionState); - return ( -
- {subscriptionKeys.map((cycle, idx) => { - return ( -
- - {idx < subscriptionKeys.length - 1 &&
} -
- ); - })} + + + + + + + diff --git a/apps/client/src/features/modals/integration-modal/OscSubscriptionRow.tsx b/apps/client/src/features/modals/integration-modal/OscSubscriptionRow.tsx index 38f15ceee..1dd53bae2 100644 --- a/apps/client/src/features/modals/integration-modal/OscSubscriptionRow.tsx +++ b/apps/client/src/features/modals/integration-modal/OscSubscriptionRow.tsx @@ -1,8 +1,10 @@ -import { UseFormRegister } from 'react-hook-form'; +import { Control, useFieldArray, UseFormRegister } from 'react-hook-form'; import { Button, IconButton, Input, Switch } from '@chakra-ui/react'; import { FiChevronUp } from '@react-icons/all-files/fi/FiChevronUp'; import { IoRemove } from '@react-icons/all-files/io5/IoRemove'; -import { OscSubscriptionOptions, TimerLifeCycle } from 'ontime-types'; +import { OscSubscription, TimerLifeCycle } from 'ontime-types'; + +import { useEmitLog } from '../../../common/stores/logger'; import collapseStyles from '../../../common/components/collapse-bar/CollapseBar.module.scss'; import styles from '../Modal.module.scss'; @@ -13,37 +15,50 @@ interface OscSubscriptionRowProps { subtitle: string; visible: boolean; setShowSection: (cycle: TimerLifeCycle) => void; - subscriptionOptions: OscSubscriptionOptions[]; - handleDelete: (cycle: TimerLifeCycle, id: string) => void; - handleAddNew: (cycle: TimerLifeCycle) => void; - register: UseFormRegister; + register: UseFormRegister; + control: Control; } export default function OscSubscriptionRow(props: OscSubscriptionRowProps) { - const { cycle, title, subtitle, visible, setShowSection, subscriptionOptions, handleDelete, handleAddNew, register } = - props; + const { cycle, title, subtitle, visible, setShowSection, register, control } = props; + const { emitError } = useEmitLog(); + const { fields, append, remove } = useFieldArray({ + name: cycle, + control, + }); - const hasTooManyOptions = subscriptionOptions.length >= 3; + const hasTooManyOptions = fields.length >= 3; const headerStyle = `${styles.splitSection} ${visible ? '' : styles.showPointer}`; - const registerPrefix = `subscriptions.${cycle}`; + + const sectionTitle = `${title} ${fields.length ? fields.length : '-'} / 3`; + + const handleAddNew = () => { + if (hasTooManyOptions) { + emitError('Maximum amount of onLoad subscriptions reached (3)'); + return; + } + append({ + message: '', + enabled: false, + }); + }; return ( <>
setShowSection(cycle)}>
- {title} + {sectionTitle} {visible && {subtitle}}
{visible && ( <> - {subscriptionOptions.map((option, idx) => ( -
- + {fields.map((subscription, index) => ( +
} - onClick={() => handleDelete(cycle, option.id)} + onClick={() => remove(index)} aria-label='delete' size='xs' colorScheme='red' @@ -52,13 +67,14 @@ export default function OscSubscriptionRow(props: OscSubscriptionRowProps) { placeholder='OSC Message' size='xs' variant='ontime-filled-on-light' - {...register(`${registerPrefix}[${idx}].message`)} + autoComplete='off' + {...register(`${cycle}.${index}.message`)} /> - +
))}