From f720c56345cb5a9b1286df11babcea8e44847c06 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Thu, 30 Nov 2023 22:28:55 +0100 Subject: [PATCH] refactor: subscription types and form register --- .../src/common/utils/__tests__/regex.test.ts | 14 ++- apps/client/src/common/utils/regex.ts | 1 + .../integration-modal/IntegrationModal.tsx | 6 +- .../{ => http}/HttpIntegration.tsx | 60 ++++-------- .../HttpSubscriptionRow.tsx} | 19 ++-- .../integration-modal/integration.utils.ts | 30 ++++++ .../{ => osc}/OscIntegration.tsx | 61 ++++-------- .../{ => osc}/OscSettings.tsx | 14 +-- .../osc/OscSubscriptionRow.tsx | 92 +++++++++++++++++++ .../src/definitions/core/HttpSettings.type.ts | 6 +- .../src/definitions/core/OscSettings.type.ts | 5 +- .../src/definitions/core/Subscription.type.ts | 3 +- packages/types/src/index.ts | 7 +- 13 files changed, 205 insertions(+), 113 deletions(-) rename apps/client/src/features/modals/integration-modal/{ => http}/HttpIntegration.tsx (75%) rename apps/client/src/features/modals/integration-modal/{SubscriptionRow.tsx => http/HttpSubscriptionRow.tsx} (82%) create mode 100644 apps/client/src/features/modals/integration-modal/integration.utils.ts rename apps/client/src/features/modals/integration-modal/{ => osc}/OscIntegration.tsx (73%) rename apps/client/src/features/modals/integration-modal/{ => osc}/OscSettings.tsx (92%) create mode 100644 apps/client/src/features/modals/integration-modal/osc/OscSubscriptionRow.tsx diff --git a/apps/client/src/common/utils/__tests__/regex.test.ts b/apps/client/src/common/utils/__tests__/regex.test.ts index 3ab98f6ad..84c65349d 100644 --- a/apps/client/src/common/utils/__tests__/regex.test.ts +++ b/apps/client/src/common/utils/__tests__/regex.test.ts @@ -1,4 +1,4 @@ -import { isIPAddress, isOnlyNumbers } from '../regex'; +import { isIPAddress, isOnlyNumbers, startsWithHttp } from '../regex'; describe('simple tests for regex', () => { test('isOnlyNumbers', () => { @@ -24,4 +24,16 @@ describe('simple tests for regex', () => { expect(isIPAddress.test(t)).toBe(false); }); }); + + test('startsWithHttp', () => { + const right = ['http://test']; + const wrong = ['https://test', 'testing', '123.0.1']; + + right.forEach((t) => { + expect(startsWithHttp.test(t)).toBe(true); + }); + wrong.forEach((t) => { + expect(startsWithHttp.test(t)).toBe(false); + }); + }); }); diff --git a/apps/client/src/common/utils/regex.ts b/apps/client/src/common/utils/regex.ts index ffc6ad5a7..0d31fa1d6 100644 --- a/apps/client/src/common/utils/regex.ts +++ b/apps/client/src/common/utils/regex.ts @@ -1,2 +1,3 @@ export const isOnlyNumbers = /^\d+$/; export const isIPAddress = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/; +export const startsWithHttp = /^http:\/\//; diff --git a/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx b/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx index 3ad5de219..ea7fad644 100644 --- a/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx +++ b/apps/client/src/features/modals/integration-modal/IntegrationModal.tsx @@ -2,9 +2,9 @@ import { ModalBody, Tab, TabList, TabPanel, TabPanels, Tabs } from '@chakra-ui/r import ModalWrapper from '../ModalWrapper'; -import HttpIntegration from './HttpIntegration'; -import OscIntegration from './OscIntegration'; -import OscSettings from './OscSettings'; +import HttpIntegration from './http/HttpIntegration'; +import OscIntegration from './osc/OscIntegration'; +import OscSettings from './osc/OscSettings'; import styles from '../Modal.module.scss'; diff --git a/apps/client/src/features/modals/integration-modal/HttpIntegration.tsx b/apps/client/src/features/modals/integration-modal/http/HttpIntegration.tsx similarity index 75% rename from apps/client/src/features/modals/integration-modal/HttpIntegration.tsx rename to apps/client/src/features/modals/integration-modal/http/HttpIntegration.tsx index c84f49dda..c740dd735 100644 --- a/apps/client/src/features/modals/integration-modal/HttpIntegration.tsx +++ b/apps/client/src/features/modals/integration-modal/http/HttpIntegration.tsx @@ -4,44 +4,15 @@ import { Switch } from '@chakra-ui/react'; import type { HTTPSettings } from 'ontime-types'; import { TimerLifeCycle } from 'ontime-types'; -import { useHttpSettings, usePostHttpSettings } from '../../../common/hooks-query/useHttpSettings'; -import { useEmitLog } from '../../../common/stores/logger'; -import ModalLoader from '../modal-loader/ModalLoader'; -import OntimeModalFooter from '../OntimeModalFooter'; +import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings'; +import { useEmitLog } from '../../../../common/stores/logger'; +import ModalLoader from '../../modal-loader/ModalLoader'; +import OntimeModalFooter from '../../OntimeModalFooter'; +import { OntimeCycle, sectionText } from '../integration.utils'; -import SubscriptionRow from './SubscriptionRow'; +import HttpSubscriptionRow from './HttpSubscriptionRow'; -import styles from '../Modal.module.scss'; - -type OntimeCycle = keyof typeof TimerLifeCycle; - -const placeholder = 'http://x.x.x.x:xxxx/api/path'; -const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string } } = { - onLoad: { - title: 'On Load', - subtitle: 'Triggers when a timer is loaded', - }, - onStart: { - title: 'On Start', - subtitle: 'Triggers when a timer starts', - }, - onPause: { - title: 'On Pause', - subtitle: 'Triggers when a running timer is paused', - }, - onStop: { - title: 'On Stop', - subtitle: 'Triggers when a running timer is stopped', - }, - onUpdate: { - title: 'On Every Second', - subtitle: 'Triggers when a running timer is updated (at least once a second, can be more)', - }, - onFinish: { - title: 'On Finish', - subtitle: 'Triggers when a running reaches 0', - }, -}; +import styles from '../../Modal.module.scss'; export default function HttpIntegration() { const { data, isFetching } = useHttpSettings(); @@ -54,6 +25,7 @@ export default function HttpIntegration() { reset, formState: { isSubmitting, isDirty, isValid }, } = useForm({ + mode: 'onBlur', defaultValues: data, values: data, resetOptions: { @@ -87,7 +59,7 @@ export default function HttpIntegration() { }, }; - console.log('debug will submit', newSettings) + console.log('debug will submit', newSettings); await mutateAsync(newSettings); } catch (error) { @@ -98,6 +70,8 @@ export default function HttpIntegration() { if (isFetching) { return ; } + + const placeholder = 'http://x.x.x.x:xxxx/api/path'; return (
@@ -107,7 +81,7 @@ export default function HttpIntegration() {
- - - - - - void; - register: UseFormRegister; - control: Control; + register: UseFormRegister; + control: Control; placeholder: string; } @@ -24,7 +25,7 @@ export default function SubscriptionRow(props: SubscriptionRowProps) { const { cycle, title, subtitle, visible, setShowSection, register, control, placeholder } = props; const { emitError } = useEmitLog(); const { fields, append, remove } = useFieldArray({ - name: cycle, + name: `subscriptions.${cycle}`, control, }); @@ -69,7 +70,9 @@ export default function SubscriptionRow(props: SubscriptionRowProps) { size='xs' variant='ontime-filled-on-light' autoComplete='off' - {...register(`subscriptions.${cycle}.${index}.message`)} + {...register(`subscriptions.${cycle}.${index}.message`, { + pattern: { value: startsWithHttp, message: 'Request address must start with http://' }, + })} /> diff --git a/apps/client/src/features/modals/integration-modal/integration.utils.ts b/apps/client/src/features/modals/integration-modal/integration.utils.ts new file mode 100644 index 000000000..8d35c42f7 --- /dev/null +++ b/apps/client/src/features/modals/integration-modal/integration.utils.ts @@ -0,0 +1,30 @@ +import { TimerLifeCycle } from 'ontime-types'; + +export type OntimeCycle = keyof typeof TimerLifeCycle; + +export const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string } } = { + onLoad: { + title: 'On Load', + subtitle: 'Triggers when a timer is loaded', + }, + onStart: { + title: 'On Start', + subtitle: 'Triggers when a timer starts', + }, + onPause: { + title: 'On Pause', + subtitle: 'Triggers when a running timer is paused', + }, + onStop: { + title: 'On Stop', + subtitle: 'Triggers when a running timer is stopped', + }, + onUpdate: { + title: 'On Every Second', + subtitle: 'Triggers when a running timer is updated (at least once a second, can be more)', + }, + onFinish: { + title: 'On Finish', + subtitle: 'Triggers when a running reaches 0', + }, +}; diff --git a/apps/client/src/features/modals/integration-modal/OscIntegration.tsx b/apps/client/src/features/modals/integration-modal/osc/OscIntegration.tsx similarity index 73% rename from apps/client/src/features/modals/integration-modal/OscIntegration.tsx rename to apps/client/src/features/modals/integration-modal/osc/OscIntegration.tsx index 8b5b5a508..0cbbf5c8c 100644 --- a/apps/client/src/features/modals/integration-modal/OscIntegration.tsx +++ b/apps/client/src/features/modals/integration-modal/osc/OscIntegration.tsx @@ -1,45 +1,17 @@ import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; -import type { Subscription } from 'ontime-types'; +import type { OscSubscription } from 'ontime-types'; import { TimerLifeCycle } from 'ontime-types'; -import useOscSettings, { usePostOscSubscriptions } from '../../../common/hooks-query/useOscSettings'; -import { useEmitLog } from '../../../common/stores/logger'; -import ModalLoader from '../modal-loader/ModalLoader'; -import OntimeModalFooter from '../OntimeModalFooter'; +import useOscSettings, { usePostOscSubscriptions } from '../../../../common/hooks-query/useOscSettings'; +import { useEmitLog } from '../../../../common/stores/logger'; +import ModalLoader from '../../modal-loader/ModalLoader'; +import OntimeModalFooter from '../../OntimeModalFooter'; +import { type OntimeCycle, sectionText } from '../integration.utils'; -import SubscriptionRow from './SubscriptionRow'; +import OscSubscriptionRow from './OscSubscriptionRow'; -import styles from '../Modal.module.scss'; - -type OntimeCycle = keyof typeof TimerLifeCycle; - -const sectionText: { [key in TimerLifeCycle]: { title: string; subtitle: string } } = { - onLoad: { - title: 'On Load', - subtitle: 'Triggers when a timer is loaded', - }, - onStart: { - title: 'On Start', - subtitle: 'Triggers when a timer starts', - }, - onPause: { - title: 'On Pause', - subtitle: 'Triggers when a running timer is paused', - }, - onStop: { - title: 'On Stop', - subtitle: 'Triggers when a running timer is stopped', - }, - onUpdate: { - title: 'On Every Second', - subtitle: 'Triggers when a running timer is updated (at least once a second, can be more)', - }, - onFinish: { - title: 'On Finish', - subtitle: 'Triggers when a running reaches 0', - }, -}; +import styles from '../../Modal.module.scss'; export default function OscIntegration() { const { data, isFetching } = useOscSettings(); @@ -51,7 +23,7 @@ export default function OscIntegration() { register, reset, formState: { isSubmitting, isDirty, isValid }, - } = useForm({ + } = useForm({ defaultValues: data.subscriptions, values: data.subscriptions, resetOptions: { @@ -71,7 +43,7 @@ export default function OscIntegration() { reset(data.subscriptions); }; - const onSubmit = async (values: Subscription) => { + const onSubmit = async (values: OscSubscription) => { try { const subscriptions = { onLoad: values.onLoad ?? [], @@ -91,10 +63,11 @@ export default function OscIntegration() { if (isFetching) { return ; } + const placeholder = 'OSC message'; return ( - - - - - - void; + register: UseFormRegister; + control: Control; + placeholder: string; +} + +export default function OscSubscriptionRow(props: OscSubscriptionRowProps) { + const { cycle, title, subtitle, visible, setShowSection, register, control, placeholder } = props; + const { emitError } = useEmitLog(); + const { fields, append, remove } = useFieldArray({ + name: cycle, + control, + }); + + const hasTooManyOptions = fields.length >= 3; + const headerStyle = `${styles.splitSection} ${visible ? '' : styles.showPointer}`; + + 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)}> +
+ {sectionTitle} + {visible && {subtitle}} +
+ +
+ {visible && ( + <> + {fields.map((subscription, index) => ( +
+ } + onClick={() => remove(index)} + aria-label='delete' + size='xs' + colorScheme='red' + /> + + +
+ ))} + + + )} + + ); +} diff --git a/packages/types/src/definitions/core/HttpSettings.type.ts b/packages/types/src/definitions/core/HttpSettings.type.ts index 8a0d45115..0109a5a8b 100644 --- a/packages/types/src/definitions/core/HttpSettings.type.ts +++ b/packages/types/src/definitions/core/HttpSettings.type.ts @@ -1,5 +1,9 @@ import { Subscription } from './Subscription.type.js'; + +type HttpSubscriptionOptions = { message: string; enabled: boolean }; +export type HttpSubscription = Subscription; + export interface HTTPSettings { enabledOut: boolean; - subscriptions: Subscription; + subscriptions: HttpSubscription; } diff --git a/packages/types/src/definitions/core/OscSettings.type.ts b/packages/types/src/definitions/core/OscSettings.type.ts index a95391e09..e080498c2 100644 --- a/packages/types/src/definitions/core/OscSettings.type.ts +++ b/packages/types/src/definitions/core/OscSettings.type.ts @@ -1,10 +1,13 @@ import { Subscription } from './Subscription.type.js'; +type OscSubscriptionOptions = { message: string; enabled: boolean }; +export type OscSubscription = Subscription; + export interface OSCSettings { portIn: number; portOut: number; targetIP: string; enabledIn: boolean; enabledOut: boolean; - subscriptions: Subscription; + subscriptions: OscSubscription; } diff --git a/packages/types/src/definitions/core/Subscription.type.ts b/packages/types/src/definitions/core/Subscription.type.ts index 5fb30fb33..c43f8b30f 100644 --- a/packages/types/src/definitions/core/Subscription.type.ts +++ b/packages/types/src/definitions/core/Subscription.type.ts @@ -1,4 +1,3 @@ import { TimerLifeCycleKey } from './TimerLifecycle.type.js'; -export type SubscriptionOptions = { message: string; enabled: boolean }; -export type Subscription = { [key in TimerLifeCycleKey]: SubscriptionOptions[] }; \ No newline at end of file +export type Subscription = { [key in TimerLifeCycleKey]: T[] }; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 750af100f..67520d8e4 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -29,11 +29,12 @@ export type { Alias } from './definitions/core/Alias.type.js'; export type { UserFields } from './definitions/core/UserFields.type.js'; // ---> Integration, Subscription -export type { Subscription, SubscriptionOptions } from './definitions/core/Subscription.type.js'; +export type { Subscription } from './definitions/core/Subscription.type.js'; + // ---> OSC -export type { OSCSettings } from './definitions/core/OscSettings.type.js'; +export type { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js'; // ---> HTTP -export type { HTTPSettings } from './definitions/core/HttpSettings.type.js'; +export type { HTTPSettings, HttpSubscription } from './definitions/core/HttpSettings.type.js'; // SERVER RESPONSES export type { NetworkInterface, GetInfo } from './api/ontime-controller/BackendResponse.type.js';