From 139b667e2029d6adcfce500a11fb9dc36ec3f1a1 Mon Sep 17 00:00:00 2001 From: Carlos Valente <34649812+cpvalente@users.noreply.github.com> Date: Fri, 12 May 2023 22:16:33 +0200 Subject: [PATCH] v2 beta5 modals (#377) * refactor: folder structure * style: modal wrapper * feat: add nordic translations --- README.md | 2 +- apps/client/src/common/api/ontimeApi.ts | 2 +- .../components/buttons/TooltipActionBtn.tsx | 3 +- .../popover-picker/PopoverPicker.module.scss | 9 +- .../input/popover-picker/PopoverPicker.tsx | 11 + .../src/common/models/OntimeSettings.ts | 5 +- apps/client/src/common/utils/dateConfig.ts | 2 +- apps/client/src/features/editors/Editor.tsx | 4 +- .../src/features/modals/AliasesModal.jsx | 298 ------------------ .../src/features/modals/AppSettingsModal.jsx | 257 --------------- .../features/modals/EventSettingsModal.jsx | 175 ---------- .../src/features/modals/Modal.module.scss | 29 +- .../client/src/features/modals/ModalInput.tsx | 29 ++ .../src/features/modals/ModalLink.module.scss | 4 + apps/client/src/features/modals/ModalLink.tsx | 8 +- .../src/features/modals/ModalManager.tsx | 70 ---- .../src/features/modals/ModalSplitInput.tsx | 29 ++ .../src/features/modals/Modals.module.scss | 203 ------------ .../OntimeModalFooter.tsx | 2 +- .../src/features/modals/SubmitContainer.jsx | 35 -- .../src/features/modals/TableOptionsModal.jsx | 139 -------- .../features/modals/ViewsSettingsModal.tsx | 197 ------------ .../integration-modal/OscIntegration.tsx | 2 +- .../modals/integration-modal/OscSettings.tsx | 3 +- .../integration-modal/OscSubscriptionRow.tsx | 14 +- .../client/src/features/modals/modalHelper.js | 1 - .../modals/settings-modal/AliasesForm.tsx | 152 +++++++++ .../modals/settings-modal/AppSettings.tsx | 120 +++++++ .../settings-modal/CuesheetSettings.tsx | 169 ++++++++++ .../modals/settings-modal/EditorSettings.tsx | 44 +++ .../modals/settings-modal/EventDataForm.tsx | 110 +++++++ .../settings-modal/InputMillisWithString.tsx | 35 ++ .../modals/settings-modal/ModalPinInput.tsx | 35 ++ .../settings-modal/SettingsModal.module.scss | 21 ++ .../modals/settings-modal/SettingsModal.tsx | 55 ++++ .../settings-modal/ViewSettingsForm.tsx | 126 ++++++++ apps/client/src/theme/OntimeAlert.ts | 11 + apps/client/src/theme/ontimeSwitch.ts | 4 +- apps/client/src/theme/theme.ts | 6 + .../src/translation/TranslationProvider.jsx | 5 + apps/client/src/translation/languages/no.ts | 18 ++ apps/client/src/translation/languages/sv.ts | 18 ++ .../src/classes/data-provider/DataProvider.ts | 9 +- .../src/controllers/ontimeController.ts | 55 ++-- .../controllers/ontimeController.validate.js | 4 +- apps/server/src/models/dataModel.ts | 36 +-- apps/server/src/utils/parserFunctions.ts | 31 +- .../types/src/definitions/DataModel.type.ts | 1 - .../src/definitions/core/Settings.type.ts | 5 +- 49 files changed, 1103 insertions(+), 1500 deletions(-) delete mode 100644 apps/client/src/features/modals/AliasesModal.jsx delete mode 100644 apps/client/src/features/modals/AppSettingsModal.jsx delete mode 100644 apps/client/src/features/modals/EventSettingsModal.jsx create mode 100644 apps/client/src/features/modals/ModalInput.tsx delete mode 100644 apps/client/src/features/modals/ModalManager.tsx create mode 100644 apps/client/src/features/modals/ModalSplitInput.tsx delete mode 100644 apps/client/src/features/modals/Modals.module.scss rename apps/client/src/features/modals/{integration-modal => }/OntimeModalFooter.tsx (95%) delete mode 100644 apps/client/src/features/modals/SubmitContainer.jsx delete mode 100644 apps/client/src/features/modals/TableOptionsModal.jsx delete mode 100644 apps/client/src/features/modals/ViewsSettingsModal.tsx create mode 100644 apps/client/src/features/modals/settings-modal/AliasesForm.tsx create mode 100644 apps/client/src/features/modals/settings-modal/AppSettings.tsx create mode 100644 apps/client/src/features/modals/settings-modal/CuesheetSettings.tsx create mode 100644 apps/client/src/features/modals/settings-modal/EditorSettings.tsx create mode 100644 apps/client/src/features/modals/settings-modal/EventDataForm.tsx create mode 100644 apps/client/src/features/modals/settings-modal/InputMillisWithString.tsx create mode 100644 apps/client/src/features/modals/settings-modal/ModalPinInput.tsx create mode 100644 apps/client/src/features/modals/settings-modal/SettingsModal.module.scss create mode 100644 apps/client/src/features/modals/settings-modal/SettingsModal.tsx create mode 100644 apps/client/src/features/modals/settings-modal/ViewSettingsForm.tsx create mode 100644 apps/client/src/theme/OntimeAlert.ts create mode 100644 apps/client/src/translation/languages/no.ts create mode 100644 apps/client/src/translation/languages/sv.ts diff --git a/README.md b/README.md index c0c894b35..f0d5a4dbd 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Ontime is an application for managing event rundowns and running stage timers. A single, locally hosted central application distributes your event information over the local network. -This enables the distribution of the data to a series of viewers and allows integration into video and control workflows, including OBS and d3. +This enables the distribution of the data to a series of views and allows integration into video and control workflows, including OBS and d3. ![App Window](https://github.com/cpvalente/ontime/blob/master/.github/aux-images/app.jpg) diff --git a/apps/client/src/common/api/ontimeApi.ts b/apps/client/src/common/api/ontimeApi.ts index 773133a30..0c32d74e1 100644 --- a/apps/client/src/common/api/ontimeApi.ts +++ b/apps/client/src/common/api/ontimeApi.ts @@ -45,7 +45,7 @@ export async function getView(): Promise { * @description HTTP request to mutate view settings * @return {Promise} */ -export async function postView(data: ViewSettings) { +export async function postViewSettings(data: ViewSettings) { return axios.post(`${ontimeURL}/views`, data); } diff --git a/apps/client/src/common/components/buttons/TooltipActionBtn.tsx b/apps/client/src/common/components/buttons/TooltipActionBtn.tsx index 4d0c666b6..6a954d504 100644 --- a/apps/client/src/common/components/buttons/TooltipActionBtn.tsx +++ b/apps/client/src/common/components/buttons/TooltipActionBtn.tsx @@ -1,7 +1,8 @@ +import { MouseEvent } from 'react'; import { IconButton, IconButtonProps, Tooltip } from '@chakra-ui/react'; interface TooltipActionBtnProps extends IconButtonProps { - clickHandler: () => void; + clickHandler: (event?: MouseEvent) => void; tooltip: string; openDelay?: number; } diff --git a/apps/client/src/common/components/input/popover-picker/PopoverPicker.module.scss b/apps/client/src/common/components/input/popover-picker/PopoverPicker.module.scss index 67ea73722..7cd2aece8 100644 --- a/apps/client/src/common/components/input/popover-picker/PopoverPicker.module.scss +++ b/apps/client/src/common/components/input/popover-picker/PopoverPicker.module.scss @@ -2,10 +2,11 @@ @use "../../../../theme/_v2Styles" as *; .swatch { - width: 32px; - height: 32px; - border-radius: 16px; - border: 1px solid $gray-300; + width: 24px; + height: 24px; + border-radius: 12px; + border: 1px solid white; + box-shadow: 0 0 0 1px $gray-300; cursor: pointer; transition-property: box-shadow; diff --git a/apps/client/src/common/components/input/popover-picker/PopoverPicker.tsx b/apps/client/src/common/components/input/popover-picker/PopoverPicker.tsx index 54bda14c6..9611e5489 100644 --- a/apps/client/src/common/components/input/popover-picker/PopoverPicker.tsx +++ b/apps/client/src/common/components/input/popover-picker/PopoverPicker.tsx @@ -1,8 +1,19 @@ import { HexAlphaColorPicker } from 'react-colorful'; +import { useController, UseControllerProps } from 'react-hook-form'; import { Popover, PopoverContent, PopoverTrigger } from '@chakra-ui/react'; +import { ViewSettings } from 'ontime-types'; import style from './PopoverPicker.module.scss'; +export function PopoverPickerRHF(props: UseControllerProps) { + const { name, control } = props; + const { + field: { onChange, value }, + } = useController({ control, name }); + + return ; +} + interface PopoverPickerProps { color: string; onChange: (color: string) => void; diff --git a/apps/client/src/common/models/OntimeSettings.ts b/apps/client/src/common/models/OntimeSettings.ts index 2839cdc4e..f234defc1 100644 --- a/apps/client/src/common/models/OntimeSettings.ts +++ b/apps/client/src/common/models/OntimeSettings.ts @@ -4,7 +4,8 @@ export const ontimePlaceholderSettings: Settings = { app: 'ontime', version: 2, serverPort: 4001, - lock: null, - pinCode: null, + editorKey: null, + operatorKey: null, timeFormat: '24', + language: 'en', }; diff --git a/apps/client/src/common/utils/dateConfig.ts b/apps/client/src/common/utils/dateConfig.ts index 27e66fbe0..228255895 100644 --- a/apps/client/src/common/utils/dateConfig.ts +++ b/apps/client/src/common/utils/dateConfig.ts @@ -42,7 +42,7 @@ export const millisToSeconds = (millis: number | null): number => { /** * @description Converts milliseconds to seconds - * @param {number} millis - time in seconds + * @param {number} millis - time in milliseconds * @returns {number} Amount in seconds */ export const millisToMinutes = (millis: number): number => { diff --git a/apps/client/src/features/editors/Editor.tsx b/apps/client/src/features/editors/Editor.tsx index 94db948d1..8031bd6ce 100644 --- a/apps/client/src/features/editors/Editor.tsx +++ b/apps/client/src/features/editors/Editor.tsx @@ -6,8 +6,8 @@ import UploadModal from '../../common/components/upload-modal/UploadModal'; import MenuBar from '../menu/MenuBar'; import AboutModal from '../modals/about-modal/AboutModal'; import IntegrationModal from '../modals/integration-modal/IntegrationModal'; -import ModalManager from '../modals/ModalManager'; import QuickStart from '../modals/quick-start/QuickStart'; +import SettingsModal from '../modals/settings-modal/SettingsModal'; import styles from './Editor.module.scss'; @@ -40,7 +40,7 @@ export default function Editor() { - +
diff --git a/apps/client/src/features/modals/AliasesModal.jsx b/apps/client/src/features/modals/AliasesModal.jsx deleted file mode 100644 index 92222d9f1..000000000 --- a/apps/client/src/features/modals/AliasesModal.jsx +++ /dev/null @@ -1,298 +0,0 @@ -/* eslint-disable jsx-a11y/anchor-has-content */ -import { useCallback, useEffect, useState } from 'react'; -import { Button, IconButton, Input, ModalBody, Tooltip } from '@chakra-ui/react'; -import { IoInformationCircleOutline } from '@react-icons/all-files/io5/IoInformationCircleOutline'; -import { IoRemove } from '@react-icons/all-files/io5/IoRemove'; -import { IoSunny } from '@react-icons/all-files/io5/IoSunny'; - -import { useEmitLog } from '@/common/stores/logger'; - -import { viewerLocations } from '../../appConstants'; -import { postAliases } from '../../common/api/ontimeApi'; -import useAliases from '../../common/hooks-query/useAliases'; -import { validateAlias } from '../../common/utils/aliases'; -import { handleLinks, host } from '../../common/utils/linkUtils'; -import { tooltipDelayFast } from '../../ontimeConfig'; - -import SubmitContainer from './SubmitContainer'; - -import style from './Modals.module.scss'; - -export default function AliasesModal() { - const { data, status, refetch } = useAliases(); - const { emitError } = useEmitLog(); - const [changed, setChanged] = useState(false); - const [submitting, setSubmitting] = useState(false); - const [aliases, setAliases] = useState([]); - - /** - * Set formdata from server state - */ - useEffect(() => { - if (data == null) return; - if (changed) return; - setAliases([...data]); - }, [changed, data]); - - /** - * Validate and submit data - */ - const submitHandler = useCallback( - async (event) => { - event.preventDefault(); - setSubmitting(true); - - const validatedAliases = [...aliases]; - let errors = false; - for (const alias of validatedAliases) { - // validate url - const isURLValid = validateAlias(alias.pathAndParams); - if (!isURLValid.status) { - alias.urlError = isURLValid.message; - errors = true; - } else { - alias.urlError = undefined; - } - // validate alias - const isAliasValid = validateAlias(alias.alias); - if (!isAliasValid.status) { - alias.aliasError = isAliasValid.message; - errors = true; - } else { - alias.aliasError = undefined; - } - } - setAliases(validatedAliases); - - if (!errors) { - try { - await postAliases(aliases); - } catch (error) { - emitError(`Error saving settings: ${error}`); - } finally { - await refetch(); - setChanged(false); - } - } - setSubmitting(false); - }, - [aliases, emitError, refetch], - ); - - /** - * Creates a new alias in state with a temporary id - */ - const addNew = useCallback(() => { - if (aliases.length > 20) { - emitError('Maximum amount of aliases reacted (20)'); - return; - } - - const emptyAlias = { - id: Math.floor(Math.random() * 1000), - enabled: false, - alias: '', - pathAndParams: '', - }; - setAliases((prevState) => [...prevState, emptyAlias]); - setChanged(true); - }, [aliases.length, emitError]); - - /** - * Deletes an alias by a given id - * @param {string} id - id of alias to delete - */ - const deleteAlias = useCallback((id) => { - setAliases((prevState) => [...prevState.filter((a) => a.id !== id)]); - setChanged(true); - }, []); - - /** - * Sets enabled flag to true / false - * @param {string} id - object id - * @param {boolean} isEnabled - whether to enable / disable flag - */ - const setEnabled = useCallback( - (id, isEnabled) => { - const aliasesState = [...aliases]; - for (const a of aliasesState) { - if (a.id === id) { - if (isEnabled) { - if (a.alias === '' || a.pathAndParams === '') { - emitError('Alias incomplete'); - break; - } - - const isRepeated = aliases.some((r) => a.alias === r.alias && r.enabled); - if (isRepeated) { - emitError('There is already an alias with this name'); - break; - } - } - a.enabled = isEnabled; - break; - } - } - setChanged(true); - setAliases(aliasesState); - }, - [aliases, emitError], - ); - - /** - * Reverts local state equals to server state - */ - const revert = async () => { - setChanged(false); - await refetch(); - }; - - /** - * Handles change of input field in local state - * @param {number} index - index of item in array - * @param {string} field - object parameter to update - * @param {string} value - new object parameter value - */ - const handleChange = useCallback( - (index, field, value) => { - const temp = [...aliases]; - temp[index][field] = value; - setAliases(temp); - setChanged(true); - }, - [aliases], - ); - - return ( - -

- Configure easy to use URL Aliases -
- 🔥 Changes take effect on save 🔥 -

-
-
-
Default URLs
- -
Custom Aliases
-
- - - URL aliases are useful in two main scenarios - - Complicated URLs -
- eg. a lower third url with some custom parameters - - - - - - - - - - - -
- Alias - Page URL
mylowerlower?bg=ff2&text=f00&size=0.6&transition=5
-
- URLs to be changed dynamically -
- eg. an unattended screen that you would need to change route from the app - - - - - - - - - - - -
- Alias - Page URL
thirdfloorpublic
-
-
- Alias - Page URL -
- {aliases.map((alias, index) => ( - - ))} - -
- -
-
- - -
- ); -} diff --git a/apps/client/src/features/modals/AppSettingsModal.jsx b/apps/client/src/features/modals/AppSettingsModal.jsx deleted file mode 100644 index 35bd0ccd7..000000000 --- a/apps/client/src/features/modals/AppSettingsModal.jsx +++ /dev/null @@ -1,257 +0,0 @@ -import { useEffect, useState } from 'react'; -import isEqual from 'react-fast-compare'; -import { - Checkbox, - FormControl, - FormLabel, - IconButton, - Input, - ModalBody, - PinInput, - PinInputField, - Select, -} from '@chakra-ui/react'; -import { FiEye } from '@react-icons/all-files/fi/FiEye'; -import { FiX } from '@react-icons/all-files/fi/FiX'; - -import { useEmitLog } from '@/common/stores/logger'; - -import { postSettings } from '../../common/api/ontimeApi'; -import TooltipActionBtn from '../../common/components/buttons/TooltipActionBtn'; -import useSettings from '../../common/hooks-query/useSettings'; -import { ontimePlaceholderSettings } from '../../common/models/OntimeSettings'; -import { useLocalEvent } from '../../common/stores/localEvent'; - -import { inputProps } from './modalHelper'; -import SubmitContainer from './SubmitContainer'; - -import style from './Modals.module.scss'; - -export default function AppSettingsModal() { - const { data, status, refetch } = useSettings(); - const { emitError, emitWarning } = useEmitLog(); - const [formData, setFormData] = useState(ontimePlaceholderSettings); - const [changed, setChanged] = useState(false); - const [submitting, setSubmitting] = useState(false); - const [hidePin, setHidePin] = useState(true); - - const eventSettings = useLocalEvent((state) => state.eventSettings); - const setLocalEventSettings = useLocalEvent((state) => state.setLocalEventSettings); - - const [formSettings, setFormSettings] = useState(eventSettings); - - /** - * Set formdata from server state - */ - useEffect(() => { - if (data == null) return; - if (changed) return; - setFormData({ - pinCode: data.pinCode, - timeFormat: data.timeFormat, - }); - }, [changed, data]); - - /** - * Validate and submit data - */ - const submitHandler = async (event) => { - event.preventDefault(); - setSubmitting(true); - const validation = { isValid: false, message: '' }; - - const hasChanged = !isEqual(formSettings, eventSettings); - if (hasChanged) { - setLocalEventSettings(formSettings); - validation.isValid = true; - } - - // we might not have changed this - if (formData.pinCode !== data.pinCode) { - // Validate fields - if (formData.pinCode === '' || formData.pinCode == null) { - validation.isValid = true; - validation.message += 'App pin code removed'; - } else { - validation.isValid = true; - validation.message += 'App pin code added'; - } - } - - if (formData.timeFormat !== data.timeFormat) { - if (formData.timeFormat === '12' || formData.timeFormat === '24') { - validation.isValid = true; - } else { - validation.isValue = false; - } - } - - let resetChange = hasChanged; - // set fields with error - if (!validation.isValid) { - emitError(`Invalid Input: ${validation.message}`); - } else { - try { - await postSettings(formData); - } catch (error) { - emitError(`Error saving settings: ${error}`); - } finally { - await refetch(); - resetChange = true; - } - validation?.message && emitWarning(validation.message); - } - if (resetChange) { - setChanged(false); - } - setSubmitting(false); - }; - - /** - * Reverts local state equals to server state - */ - const revert = async () => { - setChanged(false); - // set from context - setFormSettings(eventSettings); - await refetch(); - }; - - /** - * Handles change of input field in local state - * @param {string} field - object parameter to update - * @param {string} value - new object parameter value - */ - const handleChange = (field, value) => { - const temp = { ...formData }; - temp[field] = value; - setFormData(temp); - setChanged(true); - }; - - const disableModal = status !== 'success'; - - return ( - -

- Options related to the application -
- 🔥 Changes take effect on save 🔥 -

-
-
-
General App Settings
-
- - - Viewer Port - -
- Ontime is available at port -
-
- -
- - - Editor Pincode - -
- Protect the editor with a Pincode -
-
-
- handleChange('pinCode', value)} - > - - - - - - } - aria-label='Editor pin code' - onMouseDown={() => setHidePin(false)} - onMouseUp={() => setHidePin(true)} - isDisabled={disableModal} - /> - } - clickHandler={() => handleChange('pinCode', '')} - isDisabled={disableModal} - /> -
-
-
-
- - - Time format - -
- 12 / 24 hour format (viewers only for now) -
-
- -
-
-
Create Event Default Settings
-
- { - setFormSettings((prev) => ({ ...prev, showQuickEntry: e.target.checked })); - setChanged(true); - }} - > - Show quick entry on cursor - - { - setFormSettings((prev) => ({ ...prev, startTimeIsLastEnd: e.target.checked })); - setChanged(true); - }} - > - Start time is last end - - { - setFormSettings((prev) => ({ ...prev, defaultPublic: e.target.checked })); - setChanged(true); - }} - > - Event default public - -
-
- - -
- ); -} diff --git a/apps/client/src/features/modals/EventSettingsModal.jsx b/apps/client/src/features/modals/EventSettingsModal.jsx deleted file mode 100644 index ee0bb472e..000000000 --- a/apps/client/src/features/modals/EventSettingsModal.jsx +++ /dev/null @@ -1,175 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { FormLabel, Input, ModalBody, Textarea } from '@chakra-ui/react'; - -import { useEmitLog } from '@/common/stores/logger'; - -import { postEventData } from '../../common/api/eventDataApi'; -import useEventData from '../../common/hooks-query/useEventData'; -import { eventDataPlaceholder } from '../../common/models/EventData'; - -import { inputProps } from './modalHelper'; -import SubmitContainer from './SubmitContainer'; - -import style from './Modals.module.scss'; - -export default function SettingsModal() { - const { data, status, refetch } = useEventData(); - const { emitError } = useEmitLog(); - const [formData, setFormData] = useState(eventDataPlaceholder); - const [changed, setChanged] = useState(false); - const [submitting, setSubmitting] = useState(false); - - /** - * Set formdata from server state - */ - useEffect(() => { - if (data == null) return; - if (changed) return; - - setFormData({ - title: data.title, - publicUrl: data.publicUrl, - publicInfo: data.publicInfo, - backstageUrl: data.backstageUrl, - backstageInfo: data.backstageInfo, - }); - }, [changed, data]); - - /** - * Validate and submit data - */ - const submitHandler = useCallback( - async (event) => { - event.preventDefault(); - setSubmitting(true); - - try { - await postEventData(formData); - } catch (error) { - emitError(`Error saving event settings: ${error}`); - } finally { - await refetch(); - setChanged(false); - } - - setSubmitting(false); - }, - [emitError, formData, refetch], - ); - - /** - * Reverts local state equals to server state - */ - const revert = useCallback(async () => { - setChanged(false); - await refetch(); - }, [refetch]); - - /** - * Handles change of input field in local state - * @param {string} field - object parameter to update - * @param {string} value - new object parameter value - */ - const handleChange = useCallback( - (field, value) => { - const temp = { ...formData }; - temp[field] = value; - setFormData(temp); - setChanged(true); - }, - [formData], - ); - - return ( - -

- Options related to the running event -
- Affects rendered views -

-
-
-
Event Data
-
- Event Title - handleChange('title', event.target.value)} - /> -
-
Additional Screen Info
-
- - Public URL - -
- QR code to be shown on public screens -
-
- handleChange('publicUrl', event.target.value)} - /> -
-
- - Public Info - -
- Information to be shown on public screens -
-
-